| @@ -1806,16 +1806,33 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| } | } | ||||
| /** | /** | ||||
| * Workbench NO-HOLD release: | |||||
| * - Create pick_order + stock_out | |||||
| * Workbench NO-HOLD release (V1): | |||||
| * - Create pick_order + stock_out header | |||||
| * - Do NOT create suggested_pick_lot / stock_out_line | * - Do NOT create suggested_pick_lot / stock_out_line | ||||
| * - Do NOT update inventory_lot_line.holdQty | * - Do NOT update inventory_lot_line.holdQty | ||||
| * | * | ||||
| * Downstream should be handled by workbench services (no-hold suggestion + stock_out_line creation). | |||||
| * Downstream should be handled by workbench batch release (no-hold suggestion + stock_out_line creation), | |||||
| * or on ticket assign when using [releaseDeliveryOrderWithoutTicketNoHoldV2]. | |||||
| */ | */ | ||||
| @Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
| open fun releaseDeliveryOrderWithoutTicketNoHold(request: ReleaseDoRequest): ReleaseDoResult { | |||||
| println(" DEBUG: Starting releaseDeliveryOrderWithoutTicketNoHold for DO ID: ${request.id}") | |||||
| open fun releaseDeliveryOrderWithoutTicketNoHold(request: ReleaseDoRequest): ReleaseDoResult = | |||||
| releaseDeliveryOrderWithoutTicketNoHoldInternal(request, createStockOutHeader = true) | |||||
| /** | |||||
| * Workbench NO-HOLD release (V2): same as [releaseDeliveryOrderWithoutTicketNoHold] but **does not** create a | |||||
| * [StockOut] row. Suggested pick lots, stock out header, and stock out lines are created when the workbench | |||||
| * assigns the [delivery_order_pick_order] ([com.ffii.fpsms.modules.deliveryOrder.service.DoWorkbenchDopoAssignmentService]). | |||||
| */ | |||||
| @Transactional(rollbackFor = [Exception::class]) | |||||
| open fun releaseDeliveryOrderWithoutTicketNoHoldV2(request: ReleaseDoRequest): ReleaseDoResult = | |||||
| releaseDeliveryOrderWithoutTicketNoHoldInternal(request, createStockOutHeader = false) | |||||
| private fun releaseDeliveryOrderWithoutTicketNoHoldInternal( | |||||
| request: ReleaseDoRequest, | |||||
| createStockOutHeader: Boolean, | |||||
| ): ReleaseDoResult { | |||||
| val tag = if (createStockOutHeader) "V1" else "V2" | |||||
| println(" DEBUG: Starting releaseDeliveryOrderWithoutTicketNoHold ($tag) for DO ID: ${request.id}") | |||||
| val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id) | ||||
| ?: throw NoSuchElementException("Delivery Order not found") | ?: throw NoSuchElementException("Delivery Order not found") | ||||
| @@ -1854,14 +1871,16 @@ val inventoryLotLine = illId?.let { inventoryLotLineMap[it] } | |||||
| pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED | pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED | ||||
| pickOrderRepository.saveAndFlush(pickOrderEntity) | pickOrderRepository.saveAndFlush(pickOrderEntity) | ||||
| // Create stock out header only; stock_out_line is created by workbench service | |||||
| val stockOut = StockOut().apply { | |||||
| this.type = "do" | |||||
| this.consoPickOrderCode = consoCode | |||||
| this.status = StockOutStatus.PENDING.status | |||||
| this.handler = request.userId | |||||
| if (createStockOutHeader) { | |||||
| // Create stock out header only; stock_out_line is created by workbench service (V1 batch) or on assign (V2) | |||||
| val stockOut = StockOut().apply { | |||||
| this.type = "do" | |||||
| this.consoPickOrderCode = consoCode | |||||
| this.status = StockOutStatus.PENDING.status | |||||
| this.handler = request.userId | |||||
| } | |||||
| stockOutRepository.saveAndFlush(stockOut) | |||||
| } | } | ||||
| stockOutRepository.saveAndFlush(stockOut) | |||||
| } | } | ||||
| // Truck selection (reuse normal logic) | // Truck selection (reuse normal logic) | ||||
| @@ -13,6 +13,7 @@ import com.ffii.fpsms.modules.user.entity.UserRepository | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalTime | |||||
| @Service | @Service | ||||
| class DoPickOrderAssignmentService( | class DoPickOrderAssignmentService( | ||||
| private val doPickOrderRepository: DoPickOrderRepository, | private val doPickOrderRepository: DoPickOrderRepository, | ||||
| @@ -41,6 +42,15 @@ class DoPickOrderAssignmentService( | |||||
| // 获取日期(如果提供) | // 获取日期(如果提供) | ||||
| val requiredDate = request.requiredDate | val requiredDate = request.requiredDate | ||||
| println(" DEBUG: assignByLane - Requested date: $requiredDate") | println(" DEBUG: assignByLane - Requested date: $requiredDate") | ||||
| val requestedDepartureTime: LocalTime? = request.truckDepartureTime | |||||
| ?.takeIf { it.isNotBlank() } | |||||
| ?.let { | |||||
| try { | |||||
| LocalTime.parse(it) | |||||
| } catch (e: Exception) { | |||||
| null | |||||
| } | |||||
| } | |||||
| // 根据是否有日期参数选择不同的查询方法 | // 根据是否有日期参数选择不同的查询方法 | ||||
| val allCandidates = if (requiredDate != null) { | val allCandidates = if (requiredDate != null) { | ||||
| @@ -58,6 +68,16 @@ class DoPickOrderAssignmentService( | |||||
| ) | ) | ||||
| } | } | ||||
| .filter { it.truckLanceCode == request.truckLanceCode } | .filter { it.truckLanceCode == request.truckLanceCode } | ||||
| .let { candidates -> | |||||
| requestedDepartureTime?.let { dep -> | |||||
| candidates.filter { it.truckDepartureTime == dep } | |||||
| } ?: candidates | |||||
| } | |||||
| .let { candidates -> | |||||
| request.loadingSequence?.let { seq -> | |||||
| candidates.filter { it.loadingSequence == seq } | |||||
| } ?: candidates | |||||
| } | |||||
| .sortedBy { it.truckDepartureTime } | .sortedBy { it.truckDepartureTime } | ||||
| println(" DEBUG: Found ${allCandidates.size} candidate do_pick_orders for lane ${request.truckLanceCode}") | println(" DEBUG: Found ${allCandidates.size} candidate do_pick_orders for lane ${request.truckLanceCode}") | ||||
| @@ -86,12 +86,15 @@ class DoPickOrderQueryService( | |||||
| DoPickOrderSummaryItem( | DoPickOrderSummaryItem( | ||||
| truckDepartureTime = it.truckDepartureTime, | truckDepartureTime = it.truckDepartureTime, | ||||
| truckLanceCode = it.truckLanceCode, | truckLanceCode = it.truckLanceCode, | ||||
| // 只對 4/F 顯示/分組 loadingSequence;2/F 維持舊邏輯避免同車線被拆成多序 | |||||
| loadingSequence = if (actualStoreId == "4/F") it.loadingSequence else null, | |||||
| handledBy = it.handledBy | handledBy = it.handledBy | ||||
| ) | ) | ||||
| } + filteredCompletedRecords.map { | } + filteredCompletedRecords.map { | ||||
| DoPickOrderSummaryItem( | DoPickOrderSummaryItem( | ||||
| truckDepartureTime = it.truckDepartureTime, | truckDepartureTime = it.truckDepartureTime, | ||||
| truckLanceCode = it.truckLanceCode, | truckLanceCode = it.truckLanceCode, | ||||
| loadingSequence = if (actualStoreId == "4/F") it.loadingSequence else null, | |||||
| handledBy = it.handledBy | handledBy = it.handledBy | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -100,24 +103,33 @@ class DoPickOrderQueryService( | |||||
| val defaultTruckLaneCode = defaultTruck?.truckLanceCode ?: "" | val defaultTruckLaneCode = defaultTruck?.truckLanceCode ?: "" | ||||
| //println(" DEBUG: After filtering, ${allRecords.size} records remain (${filteredActiveRecords.size} active + ${filteredCompletedRecords.size} completed)") | //println(" DEBUG: After filtering, ${allRecords.size} records remain (${filteredActiveRecords.size} active + ${filteredCompletedRecords.size} completed)") | ||||
| val grouped = allRecords.groupBy { it.truckDepartureTime to it.truckLanceCode } | |||||
| .mapValues { (_, list) -> | |||||
| LaneBtn( | |||||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||||
| unassigned = list.count { it.handledBy == null }, | |||||
| total = list.size | |||||
| ) | |||||
| } | |||||
| val grouped = if (actualStoreId == "4/F") { | |||||
| allRecords.groupBy { Triple(it.truckDepartureTime, it.truckLanceCode, it.loadingSequence) } | |||||
| } else { | |||||
| // 2/F:回到舊分組(truckDepartureTime + truckLanceCode) | |||||
| allRecords.groupBy { Pair(it.truckDepartureTime, it.truckLanceCode) } | |||||
| }.mapValues { (_, list) -> | |||||
| LaneBtn( | |||||
| truckLanceCode = list.first().truckLanceCode ?: "", | |||||
| loadingSequence = if (actualStoreId == "4/F") list.first().loadingSequence else null, | |||||
| unassigned = list.count { it.handledBy == null }, | |||||
| total = list.size | |||||
| ) | |||||
| } | |||||
| val filteredGrouped = grouped | val filteredGrouped = grouped | ||||
| .filter { (_, laneBtn) -> | .filter { (_, laneBtn) -> | ||||
| laneBtn.truckLanceCode != defaultTruckLaneCode | laneBtn.truckLanceCode != defaultTruckLaneCode | ||||
| } | } | ||||
| val timeGroups = filteredGrouped.entries | val timeGroups = filteredGrouped.entries | ||||
| .groupBy { it.key.first } | |||||
| .groupBy { (it.key as? Triple<*, *, *>)?.first as? java.time.LocalTime ?: (it.key as Pair<*, *>).first as java.time.LocalTime? } | |||||
| .mapValues { (_, entries) -> | .mapValues { (_, entries) -> | ||||
| entries.map { it.value } | entries.map { it.value } | ||||
| .filter { it.unassigned > 0 } // filter out lanes with no unassigned orders | .filter { it.unassigned > 0 } // filter out lanes with no unassigned orders | ||||
| .sortedByDescending { it.unassigned } | |||||
| .sortedWith( | |||||
| compareByDescending<LaneBtn> { it.unassigned } | |||||
| .thenBy { it.truckLanceCode } | |||||
| .thenBy { it.loadingSequence ?: 999 } | |||||
| ) | |||||
| } | } | ||||
| .filterValues { lanes -> lanes.isNotEmpty() } | .filterValues { lanes -> lanes.isNotEmpty() } | ||||
| .toSortedMap(compareBy { it }) | .toSortedMap(compareBy { it }) | ||||
| @@ -45,6 +45,7 @@ data class LaneRow( | |||||
| data class LaneBtn( | data class LaneBtn( | ||||
| val truckLanceCode: String, | val truckLanceCode: String, | ||||
| val loadingSequence: Int? = null, | |||||
| val unassigned: Int, | val unassigned: Int, | ||||
| val total: Int | val total: Int | ||||
| ) | ) | ||||
| @@ -53,11 +54,13 @@ data class AssignByLaneRequest( | |||||
| val storeId: String, | val storeId: String, | ||||
| val truckDepartureTime: String?, // 可选:限定出车时间 | val truckDepartureTime: String?, // 可选:限定出车时间 | ||||
| val truckLanceCode: String , | val truckLanceCode: String , | ||||
| val loadingSequence: Int? = null, | |||||
| val requiredDate: LocalDate? // 必填:车道编号 | val requiredDate: LocalDate? // 必填:车道编号 | ||||
| ) | ) | ||||
| data class DoPickOrderSummaryItem( | data class DoPickOrderSummaryItem( | ||||
| val truckDepartureTime: java.time.LocalTime?, | val truckDepartureTime: java.time.LocalTime?, | ||||
| val truckLanceCode: String?, | val truckLanceCode: String?, | ||||
| val loadingSequence: Int?, | |||||
| val handledBy: Long? | val handledBy: Long? | ||||
| ) | ) | ||||
| data class DoSearchRow( | data class DoSearchRow( | ||||