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 { | open fun saveDeliveryOrders(request: M18CommonRequest): SyncResult { | ||||
| val deliveryOrdersWithType = getDeliveryOrdersWithType(request) | 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 | * Sync a single M18 shop PO / delivery order by document [code], same search pattern as | ||||
| * [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode]. | * [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. | * No M18-side "加單" filtering is used. | ||||
| * @param newOnly when true, skip if a non-deleted local DO already exists with the same `code`. | * @param newOnly when true, skip if a non-deleted local DO already exists with the same `code`. | ||||
| */ | */ | ||||
| open fun saveDeliveryOrderByCode( | open fun saveDeliveryOrderByCode( | ||||
| code: String, | code: String, | ||||
| isEtraSync: Boolean = false, | |||||
| isExtraSync: Boolean = false, | |||||
| newOnly: Boolean = false, | newOnly: Boolean = false, | ||||
| ): SyncResult { | ): SyncResult { | ||||
| if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(code)) { | if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(code)) { | ||||
| @@ -210,12 +210,12 @@ open class M18DeliveryOrderService( | |||||
| query = conds | query = conds | ||||
| ) | ) | ||||
| return saveDeliveryOrdersWithPreparedList(prepared, syncIsEtra = isEtraSync) | |||||
| return saveDeliveryOrdersWithPreparedList(prepared, syncisExtra = isExtraSync) | |||||
| } | } | ||||
| private fun saveDeliveryOrdersWithPreparedList( | private fun saveDeliveryOrdersWithPreparedList( | ||||
| deliveryOrdersWithType: M18PurchaseOrderListResponseWithType?, | deliveryOrdersWithType: M18PurchaseOrderListResponseWithType?, | ||||
| syncIsEtra: Boolean = false, | |||||
| syncisExtra: Boolean = false, | |||||
| ): SyncResult { | ): SyncResult { | ||||
| logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------") | logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------") | ||||
| @@ -303,7 +303,7 @@ open class M18DeliveryOrderService( | |||||
| handlerId = null, | handlerId = null, | ||||
| m18BeId = mainpo.beId, | m18BeId = mainpo.beId, | ||||
| deleted = mainpo.udfIsVoid == true, | deleted = mainpo.udfIsVoid == true, | ||||
| isEtra = syncIsEtra, | |||||
| isExtra = syncisExtra, | |||||
| ) | ) | ||||
| val saveDeliveryOrderResponse = | val saveDeliveryOrderResponse = | ||||
| @@ -82,14 +82,14 @@ class M18TestController ( | |||||
| @GetMapping("/test/do-by-code") | @GetMapping("/test/do-by-code") | ||||
| fun testSyncDoByCode(@RequestParam code: String): SyncResult { | 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") | @GetMapping("/test/do-by-code-extra") | ||||
| fun testSyncDoByCodeExtra(@RequestParam code: String): SyncResult { | fun testSyncDoByCodeExtra(@RequestParam code: String): SyncResult { | ||||
| // 加單 tab: only sync when it's a NEW order (not existing in local system) | // 加單 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") | @GetMapping("/test/product-by-code") | ||||
| @@ -721,23 +721,27 @@ open class ChartService( | |||||
| /** | /** | ||||
| * Staff delivery performance: daily pick ticket count and total time per staff. | * 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). | * 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( | fun getStaffDeliveryPerformance( | ||||
| startDate: LocalDate?, | startDate: LocalDate?, | ||||
| endDate: LocalDate?, | endDate: LocalDate?, | ||||
| staffNos: List<String>? | |||||
| staffNos: List<String>?, | |||||
| storeId: String?, | |||||
| storeIdNull: Boolean?, | |||||
| ): List<Map<String, Any>> { | ): List<Map<String, Any>> { | ||||
| val args = mutableMapOf<String, Any>() | val args = mutableMapOf<String, Any>() | ||||
| val startSql = if (startDate != null) { | val startSql = if (startDate != null) { | ||||
| args["startDate"] = startDate.toString() | args["startDate"] = startDate.toString() | ||||
| "AND DATE(dpor.ticketCompleteDateTime) >= :startDate" | |||||
| "AND DATE(dop.ticketCompleteDateTime) >= :startDate" | |||||
| } else "" | } else "" | ||||
| val endSql = if (endDate != null) { | val endSql = if (endDate != null) { | ||||
| args["endDate"] = endDate.toString() | args["endDate"] = endDate.toString() | ||||
| "AND DATE(dpor.ticketCompleteDateTime) <= :endDate" | |||||
| "AND DATE(dop.ticketCompleteDateTime) <= :endDate" | |||||
| } else "" | } else "" | ||||
| val staffSql = if (!staffNos.isNullOrEmpty()) { | val staffSql = if (!staffNos.isNullOrEmpty()) { | ||||
| val nos = staffNos.map { it.trim() }.filter { it.isNotBlank() } | val nos = staffNos.map { it.trim() }.filter { it.isNotBlank() } | ||||
| @@ -746,25 +750,33 @@ open class ChartService( | |||||
| "AND u.staffNo IN (:staffNos)" | "AND u.staffNo IN (:staffNos)" | ||||
| } | } | ||||
| } else "" | } else "" | ||||
| val storeSql = when { | |||||
| storeIdNull == true -> "AND dop.storeId IS NULL" | |||||
| !storeId.isNullOrBlank() -> { | |||||
| args["filterStoreId"] = storeId.trim() | |||||
| "AND dop.storeId = :filterStoreId" | |||||
| } | |||||
| else -> "" | |||||
| } | |||||
| val sql = """ | val sql = """ | ||||
| SELECT | 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( | COALESCE(SUM( | ||||
| CASE | 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 | ELSE 0 | ||||
| END | END | ||||
| ), 0) AS totalMinutes | ), 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 | ORDER BY date, orderCount DESC | ||||
| """.trimIndent() | """.trimIndent() | ||||
| return jdbcDao.queryForList(sql, args) | return jdbcDao.queryForList(sql, args) | ||||
| @@ -192,16 +192,20 @@ class ChartController( | |||||
| chartService.getStaffDeliveryPerformanceHandlers() | 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") | @GetMapping("/staff-delivery-performance") | ||||
| fun getStaffDeliveryPerformance( | fun getStaffDeliveryPerformance( | ||||
| @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?, | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?, | ||||
| @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?, | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?, | ||||
| @RequestParam(required = false) staffNo: List<String>?, | @RequestParam(required = false) staffNo: List<String>?, | ||||
| @RequestParam(required = false) storeId: String?, | |||||
| @RequestParam(required = false) storeIdNull: Boolean?, | |||||
| ): List<Map<String, Any>> = | ): List<Map<String, Any>> = | ||||
| chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo) | |||||
| chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo, storeId, storeIdNull) | |||||
| // ---------- Job order reports ---------- | // ---------- Job order reports ---------- | ||||
| @@ -64,6 +64,6 @@ open class DeliveryOrder: BaseEntity<Long>() { | |||||
| open var m18BeId: Long? = null | open var m18BeId: Long? = null | ||||
| /** 加單:由 M18「加單」專用同步標記;一般 DO 為 false */ | /** 加單:由 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 (:status is null or d.status = :status) | ||||
| and (:etaStart is null or d.estimatedArrivalDate >= :etaStart) | and (:etaStart is null or d.estimatedArrivalDate >= :etaStart) | ||||
| and (:etaEnd is null or d.estimatedArrivalDate < :etaEnd) | 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 | order by d.id desc | ||||
| """) | """) | ||||
| fun searchDoLitePage( | fun searchDoLitePage( | ||||
| @@ -120,7 +120,7 @@ fun searchDoLitePage( | |||||
| @Param("status") status: DeliveryOrderStatus?, | @Param("status") status: DeliveryOrderStatus?, | ||||
| @Param("etaStart") etaStart: LocalDateTime?, | @Param("etaStart") etaStart: LocalDateTime?, | ||||
| @Param("etaEnd") etaEnd: LocalDateTime?, | @Param("etaEnd") etaEnd: LocalDateTime?, | ||||
| @Param("isEtra") isEtra: Boolean?, | |||||
| @Param("isExtra") isExtra: Boolean?, | |||||
| pageable: Pageable | pageable: Pageable | ||||
| ): Page<DeliveryOrderInfoLite> | ): Page<DeliveryOrderInfoLite> | ||||
| @@ -136,7 +136,7 @@ fun searchDoLitePage( | |||||
| and (:status is null or d.status = :status) | and (:status is null or d.status = :status) | ||||
| and (:etaStart is null or d.estimatedArrivalDate >= :etaStart) | and (:etaStart is null or d.estimatedArrivalDate >= :etaStart) | ||||
| and (:etaEnd is null or d.estimatedArrivalDate < :etaEnd) | 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 is not null | ||||
| and d.supplier.code in :allowedSupplierCodes | and d.supplier.code in :allowedSupplierCodes | ||||
| order by d.id desc | order by d.id desc | ||||
| @@ -148,7 +148,7 @@ fun searchDoLitePageWithSupplierCodes( | |||||
| @Param("status") status: DeliveryOrderStatus?, | @Param("status") status: DeliveryOrderStatus?, | ||||
| @Param("etaStart") etaStart: LocalDateTime?, | @Param("etaStart") etaStart: LocalDateTime?, | ||||
| @Param("etaEnd") etaEnd: LocalDateTime?, | @Param("etaEnd") etaEnd: LocalDateTime?, | ||||
| @Param("isEtra") isEtra: Boolean?, | |||||
| @Param("isExtra") isExtra: Boolean?, | |||||
| @Param("allowedSupplierCodes") allowedSupplierCodes: List<String>, | @Param("allowedSupplierCodes") allowedSupplierCodes: List<String>, | ||||
| pageable: Pageable, | pageable: Pageable, | ||||
| ): Page<DeliveryOrderInfoLite> | ): Page<DeliveryOrderInfoLite> | ||||
| @@ -48,8 +48,8 @@ interface DeliveryOrderInfoLite { | |||||
| @get:Value("#{target.shop?.addr3}") | @get:Value("#{target.shop?.addr3}") | ||||
| val shopAddress: String? | val shopAddress: String? | ||||
| @get:Value("#{target.isEtra}") | |||||
| val isEtra: Boolean | |||||
| @get:Value("#{target.isExtra}") | |||||
| val isExtra: Boolean | |||||
| } | } | ||||
| data class DeliveryOrderInfoLiteDto( | data class DeliveryOrderInfoLiteDto( | ||||
| val id: Long, | val id: Long, | ||||
| @@ -61,5 +61,5 @@ data class DeliveryOrderInfoLiteDto( | |||||
| val supplierName: String?, | val supplierName: String?, | ||||
| val shopAddress: String?, | val shopAddress: String?, | ||||
| val truckLanceCode: 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 com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo | ||||
| import java.util.Locale | import java.util.Locale | ||||
| import org.slf4j.Logger | import org.slf4j.Logger | ||||
| @Service | @Service | ||||
| open class DeliveryOrderService( | open class DeliveryOrderService( | ||||
| private val deliveryOrderRepository: DeliveryOrderRepository, | private val deliveryOrderRepository: DeliveryOrderRepository, | ||||
| @@ -121,23 +120,23 @@ open class DeliveryOrderService( | |||||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | private val doPickOrderLineRepository: DoPickOrderLineRepository, | ||||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | ||||
| private val itemsRepository: ItemsRepository, | 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( | open fun searchDoLiteByPage( | ||||
| code: String?, | code: String?, | ||||
| shopName: String?, | shopName: String?, | ||||
| @@ -147,7 +146,7 @@ open class DeliveryOrderService( | |||||
| pageSize: Int?, | pageSize: Int?, | ||||
| truckLanceCode: String?, | truckLanceCode: String?, | ||||
| floor: String? = null, | floor: String? = null, | ||||
| isEtra: Boolean? = null, | |||||
| isExtra: Boolean? = null, | |||||
| ): RecordsRes<DeliveryOrderInfoLiteDto> { | ): RecordsRes<DeliveryOrderInfoLiteDto> { | ||||
| val page = (pageNum ?: 1) - 1 | val page = (pageNum ?: 1) - 1 | ||||
| @@ -169,7 +168,7 @@ open class DeliveryOrderService( | |||||
| status = statusEnum, | status = statusEnum, | ||||
| etaStart = etaStart, | etaStart = etaStart, | ||||
| etaEnd = etaEnd, | etaEnd = etaEnd, | ||||
| isEtra = isEtra, | |||||
| isExtra = isExtra, | |||||
| allowedSupplierCodes = allowedForFloor, | allowedSupplierCodes = allowedForFloor, | ||||
| pageable = PageRequest.of(0, 100_000), | pageable = PageRequest.of(0, 100_000), | ||||
| ) | ) | ||||
| @@ -181,6 +180,7 @@ open class DeliveryOrderService( | |||||
| .associateBy { it.id } | .associateBy { it.id } | ||||
| val preFilteredContent = allResult.content | val preFilteredContent = allResult.content | ||||
| val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() | |||||
| // ✅ 优化3: 收集所有需要查询的 shopId 和日期组合(只处理预过滤后的记录) | // ✅ 优化3: 收集所有需要查询的 shopId 和日期组合(只处理预过滤后的记录) | ||||
| val shopIdAndDatePairs = preFilteredContent.mapNotNull { info -> | val shopIdAndDatePairs = preFilteredContent.mapNotNull { info -> | ||||
| @@ -191,11 +191,7 @@ open class DeliveryOrderService( | |||||
| val targetDate = estimatedArrivalDate.toLocalDate() | val targetDate = estimatedArrivalDate.toLocalDate() | ||||
| val dayAbbr = getDayOfWeekAbbr(targetDate) | val dayAbbr = getDayOfWeekAbbr(targetDate) | ||||
| val supplierCode = deliveryOrder.supplier?.code | 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) | Triple(shopId, preferredFloor, dayAbbr) | ||||
| } else { | } else { | ||||
| null | null | ||||
| @@ -217,11 +213,7 @@ open class DeliveryOrderService( | |||||
| val processedRecords = preFilteredContent.map { info -> | val processedRecords = preFilteredContent.map { info -> | ||||
| val deliveryOrder = deliveryOrdersMap[info.id] | val deliveryOrder = deliveryOrdersMap[info.id] | ||||
| val supplierCode = deliveryOrder?.supplier?.code | 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 shop = deliveryOrder?.shop | ||||
| val shopId = shop?.id | val shopId = shop?.id | ||||
| val estimatedArrivalDate = info.estimatedArrivalDate | val estimatedArrivalDate = info.estimatedArrivalDate | ||||
| @@ -248,7 +240,7 @@ open class DeliveryOrderService( | |||||
| supplierName = info.supplierName, | supplierName = info.supplierName, | ||||
| shopAddress = info.shopAddress, | shopAddress = info.shopAddress, | ||||
| truckLanceCode = calculatedTruckLanceCode, | truckLanceCode = calculatedTruckLanceCode, | ||||
| isEtra = deliveryOrdersMap[info.id]?.isEtra ?: info.isEtra, | |||||
| isExtra = deliveryOrdersMap[info.id]?.isExtra ?: info.isExtra, | |||||
| ) | ) | ||||
| }.filter { dto -> | }.filter { dto -> | ||||
| val dtoTruckLanceCode = dto.truckLanceCode?.lowercase() ?: "" | val dtoTruckLanceCode = dto.truckLanceCode?.lowercase() ?: "" | ||||
| @@ -279,19 +271,16 @@ open class DeliveryOrderService( | |||||
| status = statusEnum, | status = statusEnum, | ||||
| etaStart = etaStart, | etaStart = etaStart, | ||||
| etaEnd = etaEnd, | etaEnd = etaEnd, | ||||
| isEtra = isEtra, | |||||
| isExtra = isExtra, | |||||
| allowedSupplierCodes = allowedSupplierCodes, | allowedSupplierCodes = allowedSupplierCodes, | ||||
| pageable = PageRequest.of(page.coerceAtLeast(0), size), | pageable = PageRequest.of(page.coerceAtLeast(0), size), | ||||
| ) | ) | ||||
| val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() | |||||
| val records = result.content.map { info -> | val records = result.content.map { info -> | ||||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id) | val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id) | ||||
| val supplierCode = deliveryOrder?.supplier?.code | 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 shop = deliveryOrder?.shop | ||||
| val shopId = shop?.id | val shopId = shop?.id | ||||
| val estimatedArrivalDate = info.estimatedArrivalDate | val estimatedArrivalDate = info.estimatedArrivalDate | ||||
| @@ -315,7 +304,7 @@ open class DeliveryOrderService( | |||||
| supplierName = info.supplierName, | supplierName = info.supplierName, | ||||
| shopAddress = info.shopAddress, | shopAddress = info.shopAddress, | ||||
| truckLanceCode = calculatedTruckLanceCode, | truckLanceCode = calculatedTruckLanceCode, | ||||
| isEtra = deliveryOrder?.isEtra ?: info.isEtra, | |||||
| isExtra = deliveryOrder?.isExtra ?: info.isExtra, | |||||
| ) | ) | ||||
| } | } | ||||
| @@ -338,7 +327,7 @@ open class DeliveryOrderService( | |||||
| pageSize: Int?, | pageSize: Int?, | ||||
| truckLanceCode: String?, | truckLanceCode: String?, | ||||
| floor: String? = null, | floor: String? = null, | ||||
| isEtra: Boolean? = null, | |||||
| isExtra: Boolean? = null, | |||||
| ): RecordsRes<DeliveryOrderInfoLiteDto> { | ): RecordsRes<DeliveryOrderInfoLiteDto> { | ||||
| val mode = TruckLaneSearchSpec.parse(truckLanceCode) | val mode = TruckLaneSearchSpec.parse(truckLanceCode) | ||||
| if (mode is TruckLaneSearchSpec.Mode.NoFilter) { | if (mode is TruckLaneSearchSpec.Mode.NoFilter) { | ||||
| @@ -351,7 +340,7 @@ open class DeliveryOrderService( | |||||
| pageSize, | pageSize, | ||||
| null, | null, | ||||
| floor, | floor, | ||||
| isEtra, | |||||
| isExtra, | |||||
| ) | ) | ||||
| } | } | ||||
| val pageIdx = (pageNum ?: 1).coerceAtLeast(1) - 1 | val pageIdx = (pageNum ?: 1).coerceAtLeast(1) - 1 | ||||
| @@ -367,7 +356,7 @@ open class DeliveryOrderService( | |||||
| statusEnum = statusEnum, | statusEnum = statusEnum, | ||||
| etaStart = etaStart, | etaStart = etaStart, | ||||
| etaEnd = etaEnd, | etaEnd = etaEnd, | ||||
| isEtra = isEtra, | |||||
| isExtra = isExtra, | |||||
| allowedSupplierCodes = allowedSupplierCodesForFloor(floor), | allowedSupplierCodes = allowedSupplierCodesForFloor(floor), | ||||
| lanePredicate = lanePredicate, | lanePredicate = lanePredicate, | ||||
| ) | ) | ||||
| @@ -391,7 +380,7 @@ open class DeliveryOrderService( | |||||
| pageNum: Int?, | pageNum: Int?, | ||||
| pageSize: Int?, | pageSize: Int?, | ||||
| floor: String? = null, | floor: String? = null, | ||||
| isEtra: Boolean? = null, | |||||
| isExtra: Boolean? = null, | |||||
| ): RecordsRes<DeliveryOrderInfoLiteDto> { | ): RecordsRes<DeliveryOrderInfoLiteDto> { | ||||
| val page = (pageNum ?: 1) - 1 | val page = (pageNum ?: 1) - 1 | ||||
| val size = pageSize ?: 10 | val size = pageSize ?: 10 | ||||
| @@ -406,22 +395,19 @@ open class DeliveryOrderService( | |||||
| status = statusEnum, | status = statusEnum, | ||||
| etaStart = etaStart, | etaStart = etaStart, | ||||
| etaEnd = etaEnd, | etaEnd = etaEnd, | ||||
| isEtra = isEtra, | |||||
| isExtra = isExtra, | |||||
| allowedSupplierCodes = allowedSupplierCodes, | allowedSupplierCodes = allowedSupplierCodes, | ||||
| pageable = PageRequest.of(0, 100_000), | pageable = PageRequest.of(0, 100_000), | ||||
| ) | ) | ||||
| val deliveryOrderIds = allResult.content.mapNotNull { it.id } | val deliveryOrderIds = allResult.content.mapNotNull { it.id } | ||||
| val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds).associateBy { it.id } | val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds).associateBy { it.id } | ||||
| val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() | |||||
| val processedRecords = allResult.content.map { info -> | val processedRecords = allResult.content.map { info -> | ||||
| val deliveryOrder = deliveryOrdersMap[info.id] | val deliveryOrder = deliveryOrdersMap[info.id] | ||||
| val supplierCode = deliveryOrder?.supplier?.code | 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 shop = deliveryOrder?.shop | ||||
| val shopId = shop?.id | val shopId = shop?.id | ||||
| val infoEta = info.estimatedArrivalDate | val infoEta = info.estimatedArrivalDate | ||||
| @@ -445,7 +431,7 @@ open class DeliveryOrderService( | |||||
| supplierName = info.supplierName, | supplierName = info.supplierName, | ||||
| shopAddress = info.shopAddress, | shopAddress = info.shopAddress, | ||||
| truckLanceCode = calculatedTruckLanceCode, | truckLanceCode = calculatedTruckLanceCode, | ||||
| isEtra = deliveryOrdersMap[info.id]?.isEtra ?: info.isEtra, | |||||
| isExtra = deliveryOrdersMap[info.id]?.isExtra ?: info.isExtra, | |||||
| ) | ) | ||||
| }.filter { dto -> TruckLaneSearchSpec.isUnassignedResolvedLane(dto.truckLanceCode) } | }.filter { dto -> TruckLaneSearchSpec.isUnassignedResolvedLane(dto.truckLanceCode) } | ||||
| @@ -487,7 +473,7 @@ open class DeliveryOrderService( | |||||
| estimatedArrivalDate = deliveryOrder.estimatedArrivalDate, | estimatedArrivalDate = deliveryOrder.estimatedArrivalDate, | ||||
| completeDate = deliveryOrder.completeDate, | completeDate = deliveryOrder.completeDate, | ||||
| status = deliveryOrder.status?.value, | status = deliveryOrder.status?.value, | ||||
| isEtra = deliveryOrder.isEtra, | |||||
| isExtra = deliveryOrder.isExtra, | |||||
| deliveryOrderLines = deliveryOrder.deliveryOrderLines.map { line -> | deliveryOrderLines = deliveryOrder.deliveryOrderLines.map { line -> | ||||
| DoDetailLineResponse( | DoDetailLineResponse( | ||||
| id = line.id!!, | id = line.id!!, | ||||
| @@ -808,7 +794,7 @@ open class DeliveryOrderService( | |||||
| this.handler = handler | this.handler = handler | ||||
| m18BeId = request.m18BeId | m18BeId = request.m18BeId | ||||
| this.deleted = request.deleted | this.deleted = request.deleted | ||||
| isEtra = request.isEtra ?: false | |||||
| isExtra = request.isExtra ?: false | |||||
| } | } | ||||
| val savedDeliveryOrder = deliveryOrderRepository.saveAndFlush(deliveryOrder).let { | val savedDeliveryOrder = deliveryOrderRepository.saveAndFlush(deliveryOrder).let { | ||||
| @@ -948,14 +934,10 @@ open class DeliveryOrderService( | |||||
| println(" DEBUG: Target date: $targetDate, Date prefix: $datePrefix") | println(" DEBUG: Target date: $targetDate, Date prefix: $datePrefix") | ||||
| // 新逻辑:根据 supplier code 决定楼层 | |||||
| // 如果 supplier code 是 "P06B",使用 4F,否则使用 2F | |||||
| // 新逻辑:根据 supplier code 决定楼层(清單來自 settings) | |||||
| val supplierCode = deliveryOrder.supplier?.code | 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") | 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 targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | ||||
| val supplierCode = deliveryOrder.supplier?.code | 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(" DEBUG: Floor calculation for DO ${deliveryOrder.id}") | ||||
| println(" - Supplier code: $supplierCode") | println(" - Supplier code: $supplierCode") | ||||
| @@ -1936,7 +1914,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| truckDepartureTime = effectiveTruck.departureTime, | truckDepartureTime = effectiveTruck.departureTime, | ||||
| truckLanceCode = effectiveTruck.truckLanceCode, | truckLanceCode = effectiveTruck.truckLanceCode, | ||||
| loadingSequence = effectiveTruck.loadingSequence, | 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) | // Truck selection (reuse normal logic) | ||||
| val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now() | ||||
| val supplierCode = deliveryOrder.supplier?.code | 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 truck = deliveryOrder.shop?.id?.let { shopId -> | ||||
| val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId) | ||||
| @@ -2094,7 +2070,8 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| truckDepartureTime = effectiveTruck.departureTime, | truckDepartureTime = effectiveTruck.departureTime, | ||||
| truckLanceCode = effectiveTruck.truckLanceCode, | truckLanceCode = effectiveTruck.truckLanceCode, | ||||
| loadingSequence = effectiveTruck.loadingSequence, | loadingSequence = effectiveTruck.loadingSequence, | ||||
| usedDefaultTruck = usedDefaultTruck | |||||
| usedDefaultTruck = usedDefaultTruck, | |||||
| isExtra = deliveryOrder.isExtra ?: false, | |||||
| ) | ) | ||||
| } | } | ||||
| @@ -2104,7 +2081,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| statusEnum: DeliveryOrderStatus?, | statusEnum: DeliveryOrderStatus?, | ||||
| etaStart: LocalDateTime?, | etaStart: LocalDateTime?, | ||||
| etaEnd: LocalDateTime?, | etaEnd: LocalDateTime?, | ||||
| isEtra: Boolean?, | |||||
| isExtra: Boolean?, | |||||
| allowedSupplierCodes: List<String>, | allowedSupplierCodes: List<String>, | ||||
| lanePredicate: (String?) -> Boolean, | lanePredicate: (String?) -> Boolean, | ||||
| ): List<DeliveryOrderInfoLiteDto> { | ): List<DeliveryOrderInfoLiteDto> { | ||||
| @@ -2118,7 +2095,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| status = statusEnum, | status = statusEnum, | ||||
| etaStart = etaStart, | etaStart = etaStart, | ||||
| etaEnd = etaEnd, | etaEnd = etaEnd, | ||||
| isEtra = isEtra, | |||||
| isExtra = isExtra, | |||||
| allowedSupplierCodes = allowedSupplierCodes, | allowedSupplierCodes = allowedSupplierCodes, | ||||
| pageable = PageRequest.of(dbPage, 500), | pageable = PageRequest.of(dbPage, 500), | ||||
| ) | ) | ||||
| @@ -2140,6 +2117,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| val ids = rows.mapNotNull { it.id } | val ids = rows.mapNotNull { it.id } | ||||
| if (ids.isEmpty()) return emptyList() | if (ids.isEmpty()) return emptyList() | ||||
| val deliveryOrdersMap = deliveryOrderRepository.findAllById(ids).associateBy { it.id } | val deliveryOrdersMap = deliveryOrderRepository.findAllById(ids).associateBy { it.id } | ||||
| val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists() | |||||
| val shopIdAndDatePairs = rows.mapNotNull { info -> | val shopIdAndDatePairs = rows.mapNotNull { info -> | ||||
| val d = deliveryOrdersMap[info.id] | val d = deliveryOrdersMap[info.id] | ||||
| @@ -2149,11 +2127,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| val targetDate = eta.toLocalDate() | val targetDate = eta.toLocalDate() | ||||
| val dayAbbr = getDayOfWeekAbbr(targetDate) | val dayAbbr = getDayOfWeekAbbr(targetDate) | ||||
| val supplierCode = d.supplier?.code | 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) | Triple(shopId, preferredFloor, dayAbbr) | ||||
| } else { | } else { | ||||
| null | null | ||||
| @@ -2169,11 +2143,7 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| return rows.map { info -> | return rows.map { info -> | ||||
| val deliveryOrder = deliveryOrdersMap[info.id] | val deliveryOrder = deliveryOrdersMap[info.id] | ||||
| val supplierCode = deliveryOrder?.supplier?.code | 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 shopId = deliveryOrder?.shop?.id | ||||
| val infoEta = info.estimatedArrivalDate | val infoEta = info.estimatedArrivalDate | ||||
| val calculatedTruckLanceCode = | val calculatedTruckLanceCode = | ||||
| @@ -2194,14 +2164,14 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| supplierName = info.supplierName, | supplierName = info.supplierName, | ||||
| shopAddress = info.shopAddress, | shopAddress = info.shopAddress, | ||||
| truckLanceCode = calculatedTruckLanceCode, | 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];無命中時再退回同樓層最早出發。 | * - **4F**(P06B):維持以星期縮寫篩選 [TruckRepository.findByShopIdAndStoreIdAndDayOfWeek];無命中時再退回同樓層最早出發。 | ||||
| */ | */ | ||||
| private fun resolveTruckForShopFloorAndDay( | 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 userRepository: UserRepository, | ||||
| private val pickOrderRepository: PickOrderRepository, | private val pickOrderRepository: PickOrderRepository, | ||||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | ||||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | |||||
| ) { | ) { | ||||
| private val poolSize = Runtime.getRuntime().availableProcessors() | private val poolSize = Runtime.getRuntime().availableProcessors() | ||||
| private val executor = Executors.newFixedThreadPool(min(poolSize, 4)) | private val executor = Executors.newFixedThreadPool(min(poolSize, 4)) | ||||
| @@ -140,22 +141,15 @@ class DoReleaseCoordinatorService( | |||||
| private fun updateBatchTicketNumbers() { | private fun updateBatchTicketNumbers() { | ||||
| try { | try { | ||||
| val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") | val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") | ||||
| val pfCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("s.code") | |||||
| val updateSql = """ | val updateSql = """ | ||||
| UPDATE fpsmsdb.do_pick_order dpo | UPDATE fpsmsdb.do_pick_order dpo | ||||
| INNER JOIN ( | INNER JOIN ( | ||||
| WITH PreferredFloor AS ( | WITH PreferredFloor AS ( | ||||
| SELECT | SELECT | ||||
| do.id AS deliveryOrderId, | 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 | FROM fpsmsdb.delivery_order do | ||||
| LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0 | LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0 | ||||
| WHERE do.deleted = 0 | WHERE do.deleted = 0 | ||||
| @@ -307,20 +301,13 @@ class DoReleaseCoordinatorService( | |||||
| println(" DEBUG: Getting ordered IDs for ${ids.size} orders") | println(" DEBUG: Getting ordered IDs for ${ids.size} orders") | ||||
| println(" DEBUG: First 5 IDs: ${ids.take(5)}") | println(" DEBUG: First 5 IDs: ${ids.take(5)}") | ||||
| val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") | val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate") | ||||
| val pfCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("s.code") | |||||
| val sql = """ | val sql = """ | ||||
| WITH PreferredFloor AS ( | WITH PreferredFloor AS ( | ||||
| SELECT | SELECT | ||||
| do.id AS deliveryOrderId, | 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 | FROM fpsmsdb.delivery_order do | ||||
| LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0 | LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0 | ||||
| WHERE do.id IN (${ids.joinToString(",")}) | WHERE do.id IN (${ids.joinToString(",")}) | ||||
| @@ -144,6 +144,9 @@ open class DoWorkbenchDopoAssignmentService( | |||||
| sql.append(" AND dop.loadingSequence = :loadingSequence ") | sql.append(" AND dop.loadingSequence = :loadingSequence ") | ||||
| params["loadingSequence"] = request.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. | // 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. | // This avoids forcing the frontend to refresh when a single picked candidate is concurrently assigned. | ||||
| val candidateLimit = 50 | val candidateLimit = 50 | ||||
| @@ -247,6 +250,9 @@ open class DoWorkbenchDopoAssignmentService( | |||||
| sql.append(" AND dop.loadingSequence = :loadingSequence ") | sql.append(" AND dop.loadingSequence = :loadingSequence ") | ||||
| params["loadingSequence"] = request.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 | val shouldOrderBySequenceV1 = actualStoreId == "2/F" && request.loadingSequence == null | ||||
| if (shouldOrderBySequenceV1) { | if (shouldOrderBySequenceV1) { | ||||
| sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.loadingSequence ASC, dop.id ASC LIMIT 1 ") | 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 | } else null | ||||
| } | } | ||||
| private fun isisExtraReleaseType(releaseType: String?): Boolean { | |||||
| val n = releaseType?.trim()?.lowercase().orEmpty() | |||||
| return n == "isExtra" | |||||
| } | |||||
| private fun parseDepartureTimeToSql(raw: String?): Time? { | private fun parseDepartureTimeToSql(raw: String?): Time? { | ||||
| if (raw.isNullOrBlank()) return null | if (raw.isNullOrBlank()) return null | ||||
| val s = raw.trim() | val s = raw.trim() | ||||
| @@ -1,3 +1,4 @@ | |||||
| package com.ffii.fpsms.modules.deliveryOrder.service | package com.ffii.fpsms.modules.deliveryOrder.service | ||||
| import com.ffii.core.support.JdbcDao | 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.StoreLaneSummary | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.LaneRow | 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.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.ReleasedDoPickOrderListItem | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchTicketReleaseTableResponse | import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchTicketReleaseTableResponse | ||||
| import com.ffii.fpsms.modules.user.service.UserService | import com.ffii.fpsms.modules.user.service.UserService | ||||
| @@ -670,6 +672,7 @@ return MessageResponse( | |||||
| val releaseFilterClause = when (rt) { | val releaseFilterClause = when (rt) { | ||||
| "batch" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'batch' " | "batch" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'batch' " | ||||
| "single" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'single' " | "single" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'single' " | ||||
| "isExtra" -> " AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' " | |||||
| else -> "" | else -> "" | ||||
| } | } | ||||
| val sql = """ | val sql = """ | ||||
| @@ -812,6 +815,7 @@ return MessageResponse( | |||||
| unassigned = it.unassigned, | unassigned = it.unassigned, | ||||
| total = it.total, | total = it.total, | ||||
| handlerName = it.handlerName, | handlerName = it.handlerName, | ||||
| storeId = actualStoreId, | |||||
| ) | ) | ||||
| } | } | ||||
| .sortedWith( | .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( | open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelection( | ||||
| shopName: String?, | shopName: String?, | ||||
| storeId: String?, | storeId: String?, | ||||
| truck: String?, | truck: String?, | ||||
| releaseTypeFilter: String? = null, | |||||
| ): List<ReleasedDoPickOrderListItem> = | ): 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). | * @param requiredDeliveryDate when null, uses [LocalDate.now] (calendar today). | ||||
| * When set, filters `dop.requiredDeliveryDate = :targetDate` (workbench date picker / select day). | * 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( | open fun findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( | ||||
| shopName: String?, | shopName: String?, | ||||
| storeId: String?, | storeId: String?, | ||||
| truck: String?, | truck: String?, | ||||
| requiredDeliveryDate: LocalDate? = null, | requiredDeliveryDate: LocalDate? = null, | ||||
| releaseTypeFilter: String? = null, | |||||
| ): List<ReleasedDoPickOrderListItem> = | ): 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`. | * 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) | dop.deliveryNoteCode = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode) | ||||
| } | } | ||||
| deliveryOrderPickOrderRepository.save(dop) | deliveryOrderPickOrderRepository.save(dop) | ||||
| } | } | ||||
| markDeliveryOrdersCompletedForDeliveryOrderPickOrder(deliveryOrderPickOrderId) | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = dop.id, | id = dop.id, | ||||
| code = "SUCCESS", | code = "SUCCESS", | ||||
| @@ -1468,6 +1630,7 @@ return MessageResponse( | |||||
| truck: String?, | truck: String?, | ||||
| beforeToday: Boolean, | beforeToday: Boolean, | ||||
| equalsDeliveryDate: LocalDate? = null, | equalsDeliveryDate: LocalDate? = null, | ||||
| releaseTypeFilter: String? = null, | |||||
| ): List<ReleasedDoPickOrderListItem> { | ): List<ReleasedDoPickOrderListItem> { | ||||
| val today = LocalDate.now() | val today = LocalDate.now() | ||||
| val params = mutableMapOf<String, Any>() | val params = mutableMapOf<String, Any>() | ||||
| @@ -1518,6 +1681,10 @@ return MessageResponse( | |||||
| sqlBuilder.append(" AND (dop.shopName LIKE :shopPat OR dop.shopCode LIKE :shopPat) ") | sqlBuilder.append(" AND (dop.shopName LIKE :shopPat OR dop.shopCode LIKE :shopPat) ") | ||||
| params["shopPat"] = "%${shopName.trim()}%" | 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 ") | sqlBuilder.append(" ORDER BY dop.requiredDeliveryDate, dop.truckDepartureTime, dop.truckLanceCode, dop.id ") | ||||
| val rows: List<Map<String, Any?>> = try { | val rows: List<Map<String, Any?>> = try { | ||||
| jdbcDao.queryForList(sqlBuilder.toString(), params) | jdbcDao.queryForList(sqlBuilder.toString(), params) | ||||
| @@ -1912,6 +2079,7 @@ return MessageResponse( | |||||
| tryCompleteDeliveryOrderPickOrderTicketCompleted(poId) | tryCompleteDeliveryOrderPickOrderTicketCompleted(poId) | ||||
| } | } | ||||
| } | } | ||||
| private fun registerAfterCommit(action: () -> Unit) { | private fun registerAfterCommit(action: () -> Unit) { | ||||
| if (!TransactionSynchronizationManager.isSynchronizationActive()) { | if (!TransactionSynchronizationManager.isSynchronizationActive()) { | ||||
| action() | action() | ||||
| @@ -2047,6 +2215,7 @@ return MessageResponse( | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| private fun postWorkbenchPickSideEffects(savedStockOutLine: StockOutLine, deltaQty: BigDecimal, createLedger: Boolean = true) { | private fun postWorkbenchPickSideEffects(savedStockOutLine: StockOutLine, deltaQty: BigDecimal, createLedger: Boolean = true) { | ||||
| if (deltaQty <= BigDecimal.ZERO) return | if (deltaQty <= BigDecimal.ZERO) return | ||||
| val wall0 = System.nanoTime() | val wall0 = System.nanoTime() | ||||
| @@ -2229,9 +2398,10 @@ return MessageResponse( | |||||
| throw last ?: RuntimeException("Failed to complete pick order after retries (poId=$poId)") | 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, | * 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) { | private fun tryCompleteDeliveryOrderPickOrderTicketCompleted(poId: Long) { | ||||
| val dopRow = jdbcDao.queryForMap( | val dopRow = jdbcDao.queryForMap( | ||||
| @@ -2276,8 +2446,36 @@ return MessageResponse( | |||||
| """.trimIndent(), | """.trimIndent(), | ||||
| mapOf("dopId" to dopId, "deliveryNoteCode" to newDeliveryNoteCode), | 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>) { | private fun checkWorkbenchPickOrderLineCompleted(pickOrderLineId: Long, allStockOutLines: List<StockOutLineInfo>) { | ||||
| val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return | val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return | ||||
| if (pol.status == PickOrderLineStatus.COMPLETED) 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> { | private fun exportDNLabelsReprintWorkbench(request: PrintDNLabelsReprintRequest): Map<String, Any> { | ||||
| validateWorkbenchCartonReprintRange( | validateWorkbenchCartonReprintRange( | ||||
| fromCarton = request.fromCarton, | 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( | private fun nextDeliveryOrderPickOrderTicketNo( | ||||
| requiredDate: LocalDate, | requiredDate: LocalDate, | ||||
| storeDisplay: String, | storeDisplay: String, | ||||
| ticketLetter: String, | ticketLetter: String, | ||||
| ): 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 ymd = requiredDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | ||||
| val floor = storeDisplay.replace("/", "").trim() | val floor = storeDisplay.replace("/", "").trim() | ||||
| val prefix = "TI-$ticketLetter-$ymd-$floor-" | val prefix = "TI-$ticketLetter-$ymd-$floor-" | ||||
| @@ -397,6 +400,9 @@ open class DoWorkbenchReleaseService( | |||||
| private fun nextDeliveryOrderPickOrderSingleTicketNo(requiredDate: LocalDate, storeDisplay: String): String = | private fun nextDeliveryOrderPickOrderSingleTicketNo(requiredDate: LocalDate, storeDisplay: String): String = | ||||
| nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "S") | nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "S") | ||||
| private fun nextDeliveryOrderPickOrderEtraTicketNo(requiredDate: LocalDate, storeDisplay: String): String = | |||||
| nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "E") | |||||
| private fun asyncJobType(useV2: Boolean, dopReleaseType: String): String { | private fun asyncJobType(useV2: Boolean, dopReleaseType: String): String { | ||||
| val single = dopReleaseType.equals("single", ignoreCase = true) | val single = dopReleaseType.equals("single", ignoreCase = true) | ||||
| return when { | return when { | ||||
| @@ -440,11 +446,6 @@ open class DoWorkbenchReleaseService( | |||||
| ): Int { | ): Int { | ||||
| if (results.isEmpty()) return 0 | if (results.isEmpty()) return 0 | ||||
| val releaseTypeCol = when (dopReleaseType.lowercase()) { | |||||
| "single" -> "single" | |||||
| else -> "batch" | |||||
| } | |||||
| val grouped = results.groupBy { | val grouped = results.groupBy { | ||||
| listOf( | listOf( | ||||
| it.shopId?.toString() ?: "", | it.shopId?.toString() ?: "", | ||||
| @@ -452,7 +453,8 @@ open class DoWorkbenchReleaseService( | |||||
| it.preferredFloor, | it.preferredFloor, | ||||
| it.truckId?.toString() ?: "", | it.truckId?.toString() ?: "", | ||||
| it.truckDepartureTime?.toString() ?: "", | it.truckDepartureTime?.toString() ?: "", | ||||
| it.truckLanceCode ?: "" | |||||
| it.truckLanceCode ?: "", | |||||
| it.isExtra.toString(), | |||||
| ).joinToString("|") | ).joinToString("|") | ||||
| } | } | ||||
| @@ -477,7 +479,16 @@ open class DoWorkbenchReleaseService( | |||||
| (storeId ?: "2/F").replace("/", "").trim() | (storeId ?: "2/F").replace("/", "").trim() | ||||
| } | } | ||||
| val requiredDate = first.estimatedArrivalDate ?: LocalDate.now() | 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) | nextDeliveryOrderPickOrderSingleTicketNo(requiredDate, ticketFloorSegment) | ||||
| } else { | } else { | ||||
| nextDeliveryOrderPickOrderBatchTicketNo(requiredDate, ticketFloorSegment) | nextDeliveryOrderPickOrderBatchTicketNo(requiredDate, ticketFloorSegment) | ||||
| @@ -72,7 +72,7 @@ class DeliveryOrderController( | |||||
| pageSize = request.pageSize, | pageSize = request.pageSize, | ||||
| truckLanceCode = request.truckLanceCode, | truckLanceCode = request.truckLanceCode, | ||||
| floor = request.floor, | floor = request.floor, | ||||
| isEtra = request.isEtra, | |||||
| isExtra = request.isExtra, | |||||
| ) | ) | ||||
| } | } | ||||
| @@ -89,7 +89,7 @@ class DeliveryOrderController( | |||||
| pageNum = request.pageNum, | pageNum = request.pageNum, | ||||
| pageSize = request.pageSize, | pageSize = request.pageSize, | ||||
| floor = request.floor, | floor = request.floor, | ||||
| isEtra = request.isEtra, | |||||
| isExtra = request.isExtra, | |||||
| ) | ) | ||||
| } | } | ||||
| @@ -108,7 +108,7 @@ class DeliveryOrderController( | |||||
| pageSize = request.pageSize, | pageSize = request.pageSize, | ||||
| truckLanceCode = request.truckLanceCode, | truckLanceCode = request.truckLanceCode, | ||||
| floor = request.floor, | 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`). */ | /** Past-date backlog tickets from `delivery_order_pick_order` (not `do_pick_order`). */ | ||||
| @GetMapping("/released") | @GetMapping("/released") | ||||
| fun getWorkbenchReleasedDoPickOrders( | fun getWorkbenchReleasedDoPickOrders( | ||||
| @RequestParam(required = false) shopName: String?, | @RequestParam(required = false) shopName: String?, | ||||
| @RequestParam(required = false) storeId: String?, | @RequestParam(required = false) storeId: String?, | ||||
| @RequestParam(required = false) truck: String? | |||||
| @RequestParam(required = false) truck: String?, | |||||
| @RequestParam(required = false) releaseType: String?, | |||||
| ): List<ReleasedDoPickOrderListItem> { | ): List<ReleasedDoPickOrderListItem> { | ||||
| return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelection(shopName, storeId, truck) | |||||
| return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelection( | |||||
| shopName, | |||||
| storeId, | |||||
| truck, | |||||
| releaseTypeFilter = releaseType, | |||||
| ) | |||||
| } | } | ||||
| @GetMapping("/released-today") | @GetMapping("/released-today") | ||||
| @@ -112,12 +125,14 @@ class DoWorkbenchController( | |||||
| @RequestParam(required = false) storeId: String?, | @RequestParam(required = false) storeId: String?, | ||||
| @RequestParam(required = false) truck: String?, | @RequestParam(required = false) truck: String?, | ||||
| @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?, | @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) requiredDate: LocalDate?, | ||||
| @RequestParam(required = false) releaseType: String?, | |||||
| ): List<ReleasedDoPickOrderListItem> { | ): List<ReleasedDoPickOrderListItem> { | ||||
| return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( | return doWorkbenchMainService.findWorkbenchReleasedDeliveryOrderPickOrdersForSelectionToday( | ||||
| shopName, | shopName, | ||||
| storeId, | storeId, | ||||
| truck, | truck, | ||||
| requiredDeliveryDate = requiredDate, | requiredDeliveryDate = requiredDate, | ||||
| releaseTypeFilter = releaseType, | |||||
| ) | ) | ||||
| } | } | ||||
| @@ -19,7 +19,7 @@ data class DoDetailResponse( | |||||
| val completeDate: LocalDateTime?, | val completeDate: LocalDateTime?, | ||||
| val status: String?, | val status: String?, | ||||
| /** 加單 DO(M18 加單專用同步) */ | /** 加單 DO(M18 加單專用同步) */ | ||||
| val isEtra: Boolean = false, | |||||
| val isExtra: Boolean = false, | |||||
| val deliveryOrderLines: List<DoDetailLineResponse> | val deliveryOrderLines: List<DoDetailLineResponse> | ||||
| ) | ) | ||||
| @@ -51,7 +51,18 @@ data class LaneBtn( | |||||
| val unassigned: Int, | val unassigned: Int, | ||||
| val total: Int, | val total: Int, | ||||
| // 同一 truckLanceCode + loadingSequence 的 handler 去重后逗号拼接 | // 同一 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( | data class AssignByLaneRequest( | ||||
| val userId: Long, | val userId: Long, | ||||
| @@ -59,7 +70,9 @@ data class AssignByLaneRequest( | |||||
| val truckDepartureTime: String?, // 可选:限定出车时间 | val truckDepartureTime: String?, // 可选:限定出车时间 | ||||
| val truckLanceCode: String , | val truckLanceCode: String , | ||||
| val loadingSequence: Int? = null, | 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( | data class DoPickOrderSummaryItem( | ||||
| val truckDepartureTime: java.time.LocalTime?, | val truckDepartureTime: java.time.LocalTime?, | ||||
| @@ -21,7 +21,8 @@ data class ReleaseDoResult( | |||||
| val truckDepartureTime: LocalTime?, | val truckDepartureTime: LocalTime?, | ||||
| val truckLanceCode: String?, | val truckLanceCode: String?, | ||||
| val loadingSequence: Int? | |||||
| val loadingSequence: Int?, | |||||
| val isExtra: Boolean = false, | |||||
| ) | ) | ||||
| data class SearchDeliveryOrderInfoRequest( | data class SearchDeliveryOrderInfoRequest( | ||||
| val code: String?, | val code: String?, | ||||
| @@ -31,8 +32,8 @@ data class SearchDeliveryOrderInfoRequest( | |||||
| val pageSize: Int?, | val pageSize: Int?, | ||||
| val pageNum: Int?, | val pageNum: Int?, | ||||
| val truckLanceCode: String?, | 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, | 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 handlerId: Long?, | ||||
| val m18BeId: Long?, | val m18BeId: Long?, | ||||
| val deleted: Boolean? = false, | val deleted: Boolean? = false, | ||||
| val isEtra: Boolean? = false, | |||||
| val isExtra: Boolean? = false, | |||||
| ) | ) | ||||
| data class SaveDeliveryOrderStatusRequest( | data class SaveDeliveryOrderStatusRequest( | ||||
| @@ -240,6 +240,9 @@ ORDER BY | |||||
| "id" to row["stockOutLineId"], | "id" to row["stockOutLineId"], | ||||
| "status" to row["stockOutLineStatus"], | "status" to row["stockOutLineStatus"], | ||||
| "qty" to row["stockOutLineQty"], | "qty" to row["stockOutLineQty"], | ||||
| "requiredQty" to row["requiredQty"], | |||||
| "suggestedPickLotQty" to row["requiredQty"], | |||||
| "suggestedPickLotId" to row["suggestedPickLotId"], | |||||
| "lotId" to lotId, | "lotId" to lotId, | ||||
| "lotNo" to (row["lotNo"] ?: ""), | "lotNo" to (row["lotNo"] ?: ""), | ||||
| "location" to (row["location"] ?: ""), | "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.entity.ShopRepository | ||||
| import com.ffii.fpsms.modules.master.enums.ShopType | import com.ffii.fpsms.modules.master.enums.ShopType | ||||
| import com.ffii.fpsms.modules.master.service.ItemUomService | import com.ffii.fpsms.modules.master.service.ItemUomService | ||||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService | |||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter | import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter | ||||
| import net.sf.jasperreports.export.SimpleExporterInput | import net.sf.jasperreports.export.SimpleExporterInput | ||||
| @@ -20,6 +21,7 @@ open class ReportService( | |||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| private val itemUomService: ItemUomService, | private val itemUomService: ItemUomService, | ||||
| private val shopRepository: ShopRepository, | private val shopRepository: ShopRepository, | ||||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | |||||
| ) { | ) { | ||||
| /** | /** | ||||
| * Queries the database for inventory data based on dates and optional item type. | * 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)" | "AND DATE(IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)" | ||||
| } else "" | } else "" | ||||
| val supplierFloorSqlCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("supplier.code") | |||||
| val sql = """ | val sql = """ | ||||
| SELECT | SELECT | ||||
| IFNULL(DATE_FORMAT( | IFNULL(DATE_FORMAT( | ||||
| @@ -143,17 +147,9 @@ FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty, | |||||
| FROM truck t2 | FROM truck t2 | ||||
| WHERE t2.shopId = do.shopId | WHERE t2.shopId = do.shopId | ||||
| AND t2.deleted = 0 | 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 ( | 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 | AND (SELECT COUNT(*) FROM truck t3 | ||||
| WHERE t3.shopId = do.shopId AND t3.deleted = 0 | WHERE t3.shopId = do.shopId AND t3.deleted = 0 | ||||
| AND t3.Store_id = '4F') > 1 | AND t3.Store_id = '4F') > 1 | ||||
| @@ -170,11 +166,7 @@ FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty, | |||||
| ELSE '' | ELSE '' | ||||
| END, '%')) | END, '%')) | ||||
| OR | 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 | ORDER BY t2.DepartureTime ASC | ||||
| LIMIT 1), | LIMIT 1), | ||||
| @@ -5,6 +5,7 @@ import java.util.List; | |||||
| import org.springframework.http.HttpStatus; | import org.springframework.http.HttpStatus; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.PatchMapping; | 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.PathVariable; | ||||
| import org.springframework.web.bind.annotation.RequestBody; | import org.springframework.web.bind.annotation.RequestBody; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | import org.springframework.web.bind.annotation.RequestMapping; | ||||
| @@ -41,13 +42,24 @@ public class SettingsController{ | |||||
| // @PreAuthorize("hasAuthority('ADMIN')") | // @PreAuthorize("hasAuthority('ADMIN')") | ||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | @ResponseStatus(HttpStatus.NO_CONTENT) | ||||
| public void update(@PathVariable String name, @RequestBody @Valid UpdateReq body) { | 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) | Settings entity = this.settingsService.findByName(name) | ||||
| .orElseThrow(NotFoundException::new); | .orElseThrow(NotFoundException::new); | ||||
| if (!this.settingsService.validateType(entity.getType(), body.value)) { | |||||
| if (!this.settingsService.validateType(entity.getType(), body.getValue())) { | |||||
| throw new BadRequestException(); | throw new BadRequestException(); | ||||
| } | } | ||||
| entity.setValue(body.value); | |||||
| entity.setValue(body.getValue()); | |||||
| this.settingsService.save(entity); | 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.entity.StockOutLIneRepository | ||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | ||||
| import com.ffii.fpsms.modules.common.SecurityUtils | import com.ffii.fpsms.modules.common.SecurityUtils | ||||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService | |||||
| @Service | @Service | ||||
| open class SuggestedPickLotService( | open class SuggestedPickLotService( | ||||
| val suggestedPickLotRepository: SuggestPickLotRepository, | val suggestedPickLotRepository: SuggestPickLotRepository, | ||||
| @@ -57,7 +58,8 @@ open class SuggestedPickLotService( | |||||
| val failInventoryLotLineRepository: FailInventoryLotLineRepository, | val failInventoryLotLineRepository: FailInventoryLotLineRepository, | ||||
| val stockOutRepository: StockOutRepository, | val stockOutRepository: StockOutRepository, | ||||
| val itemRepository: ItemsRepository, | val itemRepository: ItemsRepository, | ||||
| val stockOutLineRepository: StockOutLIneRepository | |||||
| val stockOutLineRepository: StockOutLIneRepository, | |||||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | |||||
| ) { | ) { | ||||
| // Calculation Available Qty / Remaining Qty | // Calculation Available Qty / Remaining Qty | ||||
| @@ -114,6 +116,8 @@ open class SuggestedPickLotService( | |||||
| .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today)} | .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today)} | ||||
| .sortedBy { it.expiryDate } | .sortedBy { it.expiryDate } | ||||
| .groupBy { it.item?.id } | .groupBy { it.item?.id } | ||||
| val (floorSuppliers2F, floorSuppliers4F) = doFloorSupplierSettingsService.loadDoFloorSupplierLists() | |||||
| // loop for suggest pick lot line | // loop for suggest pick lot line | ||||
| pols.forEach { line -> | pols.forEach { line -> | ||||
| @@ -126,11 +130,11 @@ open class SuggestedPickLotService( | |||||
| val doPreferredFloor: String? = if (isDoPickOrder) { | val doPreferredFloor: String? = if (isDoPickOrder) { | ||||
| val supplierCode = pickOrder?.deliveryOrder?.supplier?.code | val supplierCode = pickOrder?.deliveryOrder?.supplier?.code | ||||
| when (supplierCode) { | |||||
| "P06B" -> "4F" | |||||
| "P07", "P06D" -> "2F" | |||||
| else -> null // 其他供应商不限定 2F/4F | |||||
| } | |||||
| doFloorSupplierSettingsService.preferredFloorForPickLotOrNull( | |||||
| supplierCode, | |||||
| floorSuppliers2F, | |||||
| floorSuppliers4F, | |||||
| ) | |||||
| } else { | } else { | ||||
| null | 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; | |||||