| @@ -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 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]) | |||
| 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) | |||
| ?: 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 | |||
| 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) | |||
| @@ -13,6 +13,7 @@ import com.ffii.fpsms.modules.user.entity.UserRepository | |||
| import org.springframework.stereotype.Service | |||
| import java.time.LocalDateTime | |||
| import java.time.LocalDate | |||
| import java.time.LocalTime | |||
| @Service | |||
| class DoPickOrderAssignmentService( | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| @@ -41,6 +42,15 @@ class DoPickOrderAssignmentService( | |||
| // 获取日期(如果提供) | |||
| val requiredDate = request.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) { | |||
| @@ -58,6 +68,16 @@ class DoPickOrderAssignmentService( | |||
| ) | |||
| } | |||
| .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 } | |||
| println(" DEBUG: Found ${allCandidates.size} candidate do_pick_orders for lane ${request.truckLanceCode}") | |||
| @@ -86,12 +86,15 @@ class DoPickOrderQueryService( | |||
| DoPickOrderSummaryItem( | |||
| truckDepartureTime = it.truckDepartureTime, | |||
| truckLanceCode = it.truckLanceCode, | |||
| // 只對 4/F 顯示/分組 loadingSequence;2/F 維持舊邏輯避免同車線被拆成多序 | |||
| loadingSequence = if (actualStoreId == "4/F") it.loadingSequence else null, | |||
| handledBy = it.handledBy | |||
| ) | |||
| } + filteredCompletedRecords.map { | |||
| DoPickOrderSummaryItem( | |||
| truckDepartureTime = it.truckDepartureTime, | |||
| truckLanceCode = it.truckLanceCode, | |||
| loadingSequence = if (actualStoreId == "4/F") it.loadingSequence else null, | |||
| handledBy = it.handledBy | |||
| ) | |||
| } | |||
| @@ -100,24 +103,33 @@ class DoPickOrderQueryService( | |||
| val defaultTruckLaneCode = defaultTruck?.truckLanceCode ?: "" | |||
| //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 | |||
| .filter { (_, laneBtn) -> | |||
| laneBtn.truckLanceCode != defaultTruckLaneCode | |||
| } | |||
| 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) -> | |||
| entries.map { it.value } | |||
| .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() } | |||
| .toSortedMap(compareBy { it }) | |||
| @@ -45,6 +45,7 @@ data class LaneRow( | |||
| data class LaneBtn( | |||
| val truckLanceCode: String, | |||
| val loadingSequence: Int? = null, | |||
| val unassigned: Int, | |||
| val total: Int | |||
| ) | |||
| @@ -53,11 +54,13 @@ data class AssignByLaneRequest( | |||
| val storeId: String, | |||
| val truckDepartureTime: String?, // 可选:限定出车时间 | |||
| val truckLanceCode: String , | |||
| val loadingSequence: Int? = null, | |||
| val requiredDate: LocalDate? // 必填:车道编号 | |||
| ) | |||
| data class DoPickOrderSummaryItem( | |||
| val truckDepartureTime: java.time.LocalTime?, | |||
| val truckLanceCode: String?, | |||
| val loadingSequence: Int?, | |||
| val handledBy: Long? | |||
| ) | |||
| data class DoSearchRow( | |||