| @@ -86,6 +86,9 @@ open class DoReplenishment : BaseEntity<Long>() { | |||
| @Column(name = "pickOrderLineId") | |||
| open var pickOrderLineId: Long? = null | |||
| @Column(name = "deliveryOrderPickOrderId") | |||
| open var deliveryOrderPickOrderId: Long? = null | |||
| @NotNull | |||
| @Size(max = 20) | |||
| @Column(name = "status", nullable = false, length = 20) | |||
| @@ -20,6 +20,11 @@ interface DoReplenishmentRepository : AbstractRepository<DoReplenishment, Long> | |||
| fun findByTargetDoIdInAndDeletedIsFalse(targetDoIds: Collection<Long>): List<DoReplenishment> | |||
| fun findFirstByPickOrderLineIdAndStatusAndDeletedIsFalse( | |||
| pickOrderLineId: Long, | |||
| status: String, | |||
| ): DoReplenishment? | |||
| @Query( | |||
| """ | |||
| SELECT r FROM DoReplenishment r | |||
| @@ -34,6 +39,29 @@ interface DoReplenishmentRepository : AbstractRepository<DoReplenishment, Long> | |||
| @Param("status") status: String?, | |||
| ): List<DoReplenishment> | |||
| @Query( | |||
| """ | |||
| SELECT r FROM DoReplenishment r | |||
| WHERE r.deleted = false | |||
| AND r.status = :status | |||
| AND ( | |||
| :truckLaneCode IS NULL OR :truckLaneCode = '' | |||
| OR LOWER(COALESCE(r.truckLaneCode, '')) LIKE LOWER(CONCAT('%', :truckLaneCode, '%')) | |||
| ) | |||
| AND ( | |||
| :shopName IS NULL OR :shopName = '' | |||
| OR LOWER(COALESCE(r.shopName, '')) LIKE LOWER(CONCAT('%', :shopName, '%')) | |||
| OR LOWER(COALESCE(r.shopCode, '')) LIKE LOWER(CONCAT('%', :shopName, '%')) | |||
| ) | |||
| ORDER BY r.shopName, r.shopCode, r.code | |||
| """, | |||
| ) | |||
| fun searchForBatchRelease( | |||
| @Param("status") status: String, | |||
| @Param("truckLaneCode") truckLaneCode: String?, | |||
| @Param("shopName") shopName: String?, | |||
| ): List<DoReplenishment> | |||
| @Query( | |||
| """ | |||
| SELECT r.code FROM DoReplenishment r | |||
| @@ -33,6 +33,7 @@ import java.math.BigDecimal | |||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||
| import java.time.LocalDateTime | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrder | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoReplenishment | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.ExportDeliveryNoteRequest | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDeliveryNoteRequest | |||
| @@ -125,7 +126,7 @@ open class DeliveryOrderService( | |||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | |||
| private val itemsRepository: ItemsRepository, | |||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | |||
| private val doReplenishmentService: DoReplenishmentService, | |||
| @Lazy private val doReplenishmentService: DoReplenishmentService, | |||
| ) { | |||
| /** | |||
| * 樓層篩選:2F/4F/ALL 由 [DoFloorSupplierSettingsService] 讀 `settings`。 | |||
| @@ -1272,7 +1273,7 @@ open class DeliveryOrderService( | |||
| val deliveryOrders = deliveryOrderRepository.findAllById(deliveryOrderIds) | |||
| val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") } | |||
| val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra } | |||
| val replenishPdfIndex = buildReplenishPdfIndex(deliveryOrderIds) | |||
| val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(deliveryOrderIds) | |||
| val exportLines = deliveryNoteExportLines(deliveryNoteInfo) | |||
| val sortedLines = exportLines.sortedBy { row -> | |||
| @@ -1459,7 +1460,7 @@ open class DeliveryOrderService( | |||
| val deliveryOrders = deliveryOrderRepository.findAllById(deliveryOrderIds) | |||
| val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") } | |||
| val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra } | |||
| val replenishPdfIndex = buildReplenishPdfIndex(deliveryOrderIds) | |||
| val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(deliveryOrderIds) | |||
| sortedLines.forEach { row -> | |||
| fields.add( | |||
| @@ -1480,6 +1481,23 @@ open class DeliveryOrderService( | |||
| ), | |||
| ) | |||
| } | |||
| doReplenishmentService.replenishmentsWithoutDeliveryOrderLine(deliveryOrderIds, exportLines) | |||
| .sortedWith(compareBy({ it.targetDoId }, { it.itemNo }, { it.code })) | |||
| .forEach { replenishment -> | |||
| fields.add( | |||
| buildDeliveryNotePdfLineFieldForReplenishment( | |||
| replenishment = replenishment, | |||
| sequenceNumber = fields.size + 1, | |||
| pickOrderLines = pickOrderLines, | |||
| stockOutLinesByPickOrderLineId = stockOutLinesByPickOrderLineId, | |||
| illById = illById, | |||
| itemsById = itemsById, | |||
| deliveryOrderCodeById = deliveryOrderCodeById, | |||
| isExtraByDoId = isExtraByDoId, | |||
| ), | |||
| ) | |||
| } | |||
| params["dnTitle"] = "送貨單" | |||
| params["colQty"] = "已提數量" | |||
| @@ -1592,33 +1610,54 @@ open class DeliveryOrderService( | |||
| return label | |||
| } | |||
| data class ReplenishPdfIndex( | |||
| private val targetDoItemKeys: Set<Pair<Long, Long>>, | |||
| private val pickOrderLineIds: Set<Long>, | |||
| ) { | |||
| fun matches(deliveryOrderId: Long, itemId: Long?, pickOrderLineId: Long?): Boolean { | |||
| if (pickOrderLineId != null && pickOrderLineId in pickOrderLineIds) return true | |||
| val resolvedItemId = itemId ?: return false | |||
| return deliveryOrderId to resolvedItemId in targetDoItemKeys | |||
| } | |||
| companion object { | |||
| val EMPTY = ReplenishPdfIndex(emptySet(), emptySet()) | |||
| } | |||
| } | |||
| fun buildDeliveryNotePdfLineFieldForReplenishment( | |||
| replenishment: DoReplenishment, | |||
| sequenceNumber: Int, | |||
| pickOrderLines: List<PickOrderLine>, | |||
| stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>, | |||
| illById: Map<Long, InventoryLotLine>, | |||
| itemsById: Map<Long, Items>, | |||
| deliveryOrderCodeById: Map<Long, String>, | |||
| isExtraByDoId: Map<Long, Boolean>, | |||
| headerTicketNo: String? = null, | |||
| headerIsMerge: Boolean = false, | |||
| ): MutableMap<String, Any> { | |||
| val deliveryOrderId = replenishment.targetDoId!! | |||
| val polId = replenishment.pickOrderLineId!! | |||
| val polIdsForRow = listOf(polId) | |||
| val pol = pickOrderLines.firstOrNull { it.id == polId } | |||
| val itemId = replenishment.itemId | |||
| val isExtra = isExtraDeliveryTicket( | |||
| lineTicketNo = headerTicketNo, | |||
| deliveryOrderIsExtra = isExtraByDoId[deliveryOrderId] == true, | |||
| headerIsMerge = headerIsMerge, | |||
| ) | |||
| fun buildReplenishPdfIndex(deliveryOrderIds: Collection<Long>): ReplenishPdfIndex { | |||
| if (deliveryOrderIds.isEmpty()) return ReplenishPdfIndex.EMPTY | |||
| val records = doReplenishmentService.findReplenishmentsByTargetDoIds(deliveryOrderIds) | |||
| if (records.isEmpty()) return ReplenishPdfIndex.EMPTY | |||
| return ReplenishPdfIndex( | |||
| targetDoItemKeys = records.mapNotNull { row -> | |||
| val targetDoId = row.targetDoId | |||
| val itemId = row.itemId | |||
| if (targetDoId != null && itemId != null) targetDoId to itemId else null | |||
| }.toSet(), | |||
| pickOrderLineIds = records.mapNotNull { it.pickOrderLineId }.toSet(), | |||
| val field = mutableMapOf<String, Any>() | |||
| field["sequenceNumber"] = formatSequenceNumber(sequenceNumber, isExtra, isReplenish = true) | |||
| field["deliveryOrderCode"] = formatDoCodeWithUnderlineLast4( | |||
| deliveryOrderCodeById[deliveryOrderId].orEmpty(), | |||
| ) | |||
| field["itemNo"] = replenishment.itemNo ?: pol?.item?.code ?: "" | |||
| field["itemName"] = replenishment.itemName ?: pol?.item?.name ?: "" | |||
| field["uom"] = pol?.uom?.udfudesc ?: "" | |||
| field["shortName"] = pol?.uom?.udfShortDesc ?: "" | |||
| field["qty"] = sumActualPickQtyForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId) | |||
| field["route"] = when { | |||
| itemId != null -> { | |||
| routeFromStockOutsForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId, illById) | |||
| .takeIf { it != "-" } ?: getWarehouseCodeByItemId(itemId) ?: "-" | |||
| } | |||
| else -> "-" | |||
| } | |||
| field["lotNo"] = lotNumbersForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId) | |||
| .ifBlank { "沒有庫存" } | |||
| field["signOff"] = if (itemId != null && itemsById[itemId]?.isEgg == true) { | |||
| "簽署: __________" | |||
| } else { | |||
| "" | |||
| } | |||
| return field | |||
| } | |||
| fun buildDeliveryNotePdfLineField( | |||
| @@ -1634,7 +1673,7 @@ open class DeliveryOrderService( | |||
| isExtraByDoId: Map<Long, Boolean> = emptyMap(), | |||
| headerTicketNo: String? = null, | |||
| headerIsMerge: Boolean = false, | |||
| replenishPdfIndex: ReplenishPdfIndex = ReplenishPdfIndex.EMPTY, | |||
| replenishPdfIndex: DoReplenishmentService.ReplenishPdfIndex = DoReplenishmentService.ReplenishPdfIndex.EMPTY, | |||
| ): MutableMap<String, Any> { | |||
| val field = mutableMapOf<String, Any>() | |||
| val isExtra = isExtraDeliveryTicket( | |||
| @@ -1,5 +1,6 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.service | |||
| import com.ffii.core.support.JdbcDao | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoReplenishment | |||
| @@ -9,9 +10,15 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderLineRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.DoReplenishmentResponse | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoResult | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.SubmitDoReplenishmentLineRequest | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.SubmitDoReplenishmentRequest | |||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||
| import com.ffii.fpsms.modules.master.entity.UomConversionRepository | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus | |||
| import org.springframework.stereotype.Service | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import java.math.BigDecimal | |||
| @@ -26,6 +33,10 @@ open class DoReplenishmentService( | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||
| private val uomConversionRepository: UomConversionRepository, | |||
| private val pickOrderRepository: PickOrderRepository, | |||
| private val pickOrderLineRepository: PickOrderLineRepository, | |||
| private val itemsRepository: ItemsRepository, | |||
| private val jdbcDao: JdbcDao, | |||
| ) { | |||
| @Transactional | |||
| @@ -110,11 +121,231 @@ open class DoReplenishmentService( | |||
| return toResponses(rows) | |||
| } | |||
| open fun listForBatchRelease( | |||
| truckLaneCode: String?, | |||
| shopName: String?, | |||
| ): List<DoReplenishmentResponse> { | |||
| val truck = truckLaneCode?.trim()?.takeIf { it.isNotEmpty() } | |||
| val shop = shopName?.trim()?.takeIf { it.isNotEmpty() } | |||
| if (truck == null && shop == null) { | |||
| return emptyList() | |||
| } | |||
| val rows = doReplenishmentRepository.searchForBatchRelease( | |||
| status = DoReplenishment.STATUS_PENDING, | |||
| truckLaneCode = truck, | |||
| shopName = shop, | |||
| ) | |||
| return toResponses(rows) | |||
| } | |||
| open fun findReplenishmentsByTargetDoIds(targetDoIds: Collection<Long>): List<DoReplenishment> { | |||
| if (targetDoIds.isEmpty()) return emptyList() | |||
| return doReplenishmentRepository.findByTargetDoIdInAndDeletedIsFalse(targetDoIds) | |||
| } | |||
| data class ReplenishPdfIndex( | |||
| private val targetDoItemKeys: Set<Pair<Long, Long>>, | |||
| private val pickOrderLineIds: Set<Long>, | |||
| ) { | |||
| fun matches(deliveryOrderId: Long, itemId: Long?, pickOrderLineId: Long?): Boolean { | |||
| if (pickOrderLineId != null && pickOrderLineId in pickOrderLineIds) return true | |||
| val resolvedItemId = itemId ?: return false | |||
| return deliveryOrderId to resolvedItemId in targetDoItemKeys | |||
| } | |||
| companion object { | |||
| val EMPTY = ReplenishPdfIndex(emptySet(), emptySet()) | |||
| } | |||
| } | |||
| open fun buildReplenishPdfIndex(deliveryOrderIds: Collection<Long>): ReplenishPdfIndex { | |||
| if (deliveryOrderIds.isEmpty()) return ReplenishPdfIndex.EMPTY | |||
| val records = doReplenishmentRepository.findByTargetDoIdInAndDeletedIsFalse(deliveryOrderIds) | |||
| .filter { it.pickOrderLineId != null } | |||
| if (records.isEmpty()) return ReplenishPdfIndex.EMPTY | |||
| return ReplenishPdfIndex( | |||
| targetDoItemKeys = records.mapNotNull { row -> | |||
| val targetDoId = row.targetDoId | |||
| val itemId = row.itemId | |||
| if (targetDoId != null && itemId != null) targetDoId to itemId else null | |||
| }.toSet(), | |||
| pickOrderLineIds = records.mapNotNull { it.pickOrderLineId }.toSet(), | |||
| ) | |||
| } | |||
| /** Replenishment POL rows with no matching DOL on the target DO — append as extra DN lines. */ | |||
| open fun replenishmentsWithoutDeliveryOrderLine( | |||
| deliveryOrderIds: Collection<Long>, | |||
| exportLines: List<DeliveryOrderService.DeliveryNoteExportLine>, | |||
| ): List<DoReplenishment> { | |||
| if (deliveryOrderIds.isEmpty()) return emptyList() | |||
| val dolItemKeys = exportLines.mapNotNull { row -> | |||
| row.line.itemId?.let { row.deliveryOrderId to it } | |||
| }.toSet() | |||
| return doReplenishmentRepository.findByTargetDoIdInAndDeletedIsFalse(deliveryOrderIds) | |||
| .filter { replenishment -> | |||
| val polId = replenishment.pickOrderLineId ?: return@filter false | |||
| val targetDoId = replenishment.targetDoId ?: return@filter false | |||
| val itemId = replenishment.itemId ?: return@filter false | |||
| polId > 0 && (targetDoId to itemId) !in dolItemKeys | |||
| } | |||
| } | |||
| @Transactional | |||
| open fun completeByPickOrderLineId(pickOrderLineId: Long) { | |||
| val row = doReplenishmentRepository.findFirstByPickOrderLineIdAndStatusAndDeletedIsFalse( | |||
| pickOrderLineId, | |||
| DoReplenishment.STATUS_PROCESSING, | |||
| ) ?: return | |||
| row.status = DoReplenishment.STATUS_COMPLETED | |||
| doReplenishmentRepository.save(row) | |||
| } | |||
| /** | |||
| * After workbench batch release links pick orders to a ticket, create replenishment POL rows | |||
| * (no DOL), assign target DO / ticket FKs, and move status pending → processing. | |||
| * | |||
| * @return pick order ids that received new replenishment lines (for V1 downstream rebuild) | |||
| */ | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| open fun releasePendingReplenishmentsForWorkbenchBatch( | |||
| releasedResults: List<ReleaseDoResult>, | |||
| ): Set<Long> { | |||
| if (releasedResults.isEmpty()) return emptySet() | |||
| val pending = findPendingReplenishmentsForReleasedResults(releasedResults) | |||
| if (pending.isEmpty()) return emptySet() | |||
| val affectedPickOrderIds = mutableSetOf<Long>() | |||
| for (replenishment in pending) { | |||
| val matchingResults = releasedResults.filter { replenishmentMatchesResult(replenishment, it) } | |||
| if (matchingResults.isEmpty()) continue | |||
| val targetResult = matchingResults.minByOrNull { it.deliveryOrderId } | |||
| ?: continue | |||
| val dopoId = resolveDeliveryOrderPickOrderId(targetResult.pickOrderId) | |||
| ?: continue | |||
| val ticketPickOrderIds = pickOrderRepository.findIdsByDeliveryOrderPickOrderId(dopoId) | |||
| if (ticketPickOrderIds.isEmpty()) continue | |||
| val itemId = replenishment.itemId ?: continue | |||
| val targetPickOrderId = resolveTargetPickOrderId(ticketPickOrderIds, itemId) | |||
| val pickOrder = pickOrderRepository.findById(targetPickOrderId).orElse(null) ?: continue | |||
| val item = itemsRepository.findById(itemId).orElse(null) ?: continue | |||
| val uom = replenishment.uomId?.let { uomConversionRepository.findById(it).orElse(null) } | |||
| ?: continue | |||
| val pol = PickOrderLine().apply { | |||
| this.pickOrder = pickOrder | |||
| this.item = item | |||
| this.qty = replenishment.replenishQty | |||
| this.uom = uom | |||
| this.status = PickOrderLineStatus.PENDING | |||
| } | |||
| val savedPol = pickOrderLineRepository.save(pol) | |||
| pickOrder.totalLines = (pickOrder.totalLines ?: 0) + 1 | |||
| pickOrderRepository.save(pickOrder) | |||
| replenishment.targetDoId = targetResult.deliveryOrderId | |||
| replenishment.targetDoCode = targetResult.deliveryOrderCode | |||
| replenishment.pickOrderLineId = savedPol.id | |||
| replenishment.deliveryOrderPickOrderId = dopoId | |||
| replenishment.status = DoReplenishment.STATUS_PROCESSING | |||
| doReplenishmentRepository.save(replenishment) | |||
| affectedPickOrderIds += targetPickOrderId | |||
| } | |||
| return affectedPickOrderIds | |||
| } | |||
| private fun findPendingReplenishmentsForReleasedResults( | |||
| releasedResults: List<ReleaseDoResult>, | |||
| ): List<DoReplenishment> { | |||
| val pairs = releasedResults | |||
| .map { result -> | |||
| (result.shopName?.trim()?.takeIf { it.isNotEmpty() } | |||
| ?: result.shopCode?.trim()?.takeIf { it.isNotEmpty() } | |||
| ?: "") to (result.truckLanceCode?.trim()?.takeIf { it.isNotEmpty() } ?: "") | |||
| } | |||
| .distinct() | |||
| .filter { (shop, truck) -> shop.isNotEmpty() || truck.isNotEmpty() } | |||
| if (pairs.isEmpty()) return emptyList() | |||
| val byId = linkedMapOf<Long, DoReplenishment>() | |||
| for ((shop, truck) in pairs) { | |||
| val rows = doReplenishmentRepository.searchForBatchRelease( | |||
| status = DoReplenishment.STATUS_PENDING, | |||
| truckLaneCode = truck.takeIf { it.isNotEmpty() }, | |||
| shopName = shop.takeIf { it.isNotEmpty() }, | |||
| ) | |||
| for (row in rows) { | |||
| val id = row.id ?: continue | |||
| if (releasedResults.any { replenishmentMatchesResult(row, it) }) { | |||
| byId[id] = row | |||
| } | |||
| } | |||
| } | |||
| return byId.values.toList() | |||
| } | |||
| private fun replenishmentMatchesResult( | |||
| replenishment: DoReplenishment, | |||
| result: ReleaseDoResult, | |||
| ): Boolean { | |||
| val doTruck = normalizeText(result.truckLanceCode) | |||
| val recordTruck = normalizeText(replenishment.truckLaneCode) | |||
| if (doTruck.isNotEmpty()) { | |||
| if (recordTruck.isEmpty() || recordTruck != doTruck) return false | |||
| } | |||
| val doShopToken = shopTokenFromResult(result) | |||
| if (doShopToken.isEmpty()) return false | |||
| val recordShopCode = normalizeText(replenishment.shopCode) | |||
| val recordShopName = normalizeText(replenishment.shopName) | |||
| return recordShopCode == doShopToken || | |||
| recordShopName.startsWith(doShopToken) || | |||
| (recordShopCode.isNotEmpty() && doShopToken.startsWith(recordShopCode)) || | |||
| (recordShopCode.isNotEmpty() && recordShopCode.startsWith(doShopToken)) | |||
| } | |||
| private fun shopTokenFromResult(result: ReleaseDoResult): String { | |||
| val raw = result.shopCode?.trim()?.takeIf { it.isNotEmpty() } | |||
| ?: result.shopName?.trim() | |||
| ?: "" | |||
| if (raw.isEmpty()) return "" | |||
| return normalizeText(raw.split(" - ").firstOrNull() ?: raw) | |||
| } | |||
| private fun normalizeText(value: String?): String = value?.trim()?.lowercase() ?: "" | |||
| private fun resolveDeliveryOrderPickOrderId(pickOrderId: Long): Long? { | |||
| return jdbcDao.queryForList( | |||
| """ | |||
| SELECT deliveryOrderPickOrderId AS dopoId | |||
| FROM fpsmsdb.pick_order | |||
| WHERE id = :pickOrderId AND deleted = 0 | |||
| """.trimIndent(), | |||
| mapOf("pickOrderId" to pickOrderId), | |||
| ).firstOrNull() | |||
| ?.get("dopoId") | |||
| ?.let { (it as Number).toLong() } | |||
| } | |||
| /** Prefer pick_order that already has the same item on this ticket; else smallest pick_order.id. */ | |||
| private fun resolveTargetPickOrderId(ticketPickOrderIds: List<Long>, itemId: Long): Long { | |||
| val sortedIds = ticketPickOrderIds.sorted() | |||
| val lines = pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(sortedIds) | |||
| val pickOrderWithItem = lines | |||
| .filter { it.item?.id == itemId } | |||
| .mapNotNull { it.pickOrder?.id } | |||
| .minOrNull() | |||
| return pickOrderWithItem ?: sortedIds.first() | |||
| } | |||
| private fun nextCodeSequence(deliveryDate: LocalDate): Int { | |||
| val prefix = codePrefix(deliveryDate) | |||
| val suffixPattern = Regex("^${Regex.escape(prefix)}(\\d+)$") | |||
| @@ -206,6 +437,7 @@ open class DoReplenishmentService( | |||
| targetDoId = row.targetDoId, | |||
| targetDoCode = row.targetDoCode, | |||
| pickOrderLineId = row.pickOrderLineId, | |||
| deliveryOrderPickOrderId = row.deliveryOrderPickOrderId, | |||
| status = row.status, | |||
| created = row.created, | |||
| ) | |||
| @@ -7,6 +7,7 @@ import com.ffii.fpsms.modules.bag.service.BagService | |||
| import com.ffii.fpsms.modules.bag.web.model.CreateBagLotLineRequest | |||
| import com.ffii.fpsms.modules.common.CodeGenerator | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoReplenishment | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | |||
| @@ -130,6 +131,7 @@ open class DoWorkbenchMainService( | |||
| private val printerService: PrinterService, | |||
| private val itemsRepository: ItemsRepository, | |||
| private val transactionManager: PlatformTransactionManager, | |||
| private val doReplenishmentService: DoReplenishmentService, | |||
| ) { | |||
| @PersistenceContext | |||
| private lateinit var entityManager: EntityManager | |||
| @@ -2370,6 +2372,7 @@ return MessageResponse( | |||
| val changed = pickOrderLineRepository.markCompletedIfNotCompleted(pickOrderLineId) | |||
| if (changed > 0) { | |||
| pickOrderRepository.incrementSubmittedLines(poId) | |||
| doReplenishmentService.completeByPickOrderLineId(pickOrderLineId) | |||
| } | |||
| } | |||
| @@ -2492,6 +2495,7 @@ return MessageResponse( | |||
| pickOrderLineRepository.save( | |||
| pol.apply { this.status = PickOrderLineStatus.COMPLETED }, | |||
| ) | |||
| doReplenishmentService.completeByPickOrderLineId(pickOrderLineId) | |||
| } | |||
| } | |||
| @@ -2764,7 +2768,7 @@ return MessageResponse( | |||
| val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra } | |||
| val headerIsMerge = ctx.header.ticketNo?.startsWith("TI-M-") == true | |||
| val headerTicketNo = ctx.header.ticketNo | |||
| val replenishPdfIndex = deliveryOrderService.buildReplenishPdfIndex(ctx.deliveryOrderIds) | |||
| val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(ctx.deliveryOrderIds) | |||
| sortedLines.forEach { row -> | |||
| fields.add( | |||
| @@ -2786,6 +2790,33 @@ return MessageResponse( | |||
| ) | |||
| } | |||
| val replenishmentOnlyRows = doReplenishmentService.replenishmentsWithoutDeliveryOrderLine( | |||
| ctx.deliveryOrderIds, | |||
| exportLines, | |||
| ) | |||
| replenishmentOnlyRows.sortedWith( | |||
| compareBy<DoReplenishment>( | |||
| { it.targetDoId }, | |||
| { it.itemNo }, | |||
| { it.code }, | |||
| ), | |||
| ).forEach { replenishment -> | |||
| fields.add( | |||
| deliveryOrderService.buildDeliveryNotePdfLineFieldForReplenishment( | |||
| replenishment = replenishment, | |||
| sequenceNumber = fields.size + 1, | |||
| pickOrderLines = pickOrderLines, | |||
| stockOutLinesByPickOrderLineId = stockOutLinesByPickOrderLineId, | |||
| illById = illById, | |||
| itemsById = itemsById, | |||
| deliveryOrderCodeById = deliveryOrderCodeById, | |||
| isExtraByDoId = isExtraByDoId, | |||
| headerTicketNo = headerTicketNo, | |||
| headerIsMerge = headerIsMerge, | |||
| ), | |||
| ) | |||
| } | |||
| params["dnTitle"] = "送貨單" | |||
| params["colQty"] = "已提數量" | |||
| params["totalCartonTitle"] = "總箱數:" | |||
| @@ -90,6 +90,7 @@ data class WorkbenchBatchReleaseJobStatus( | |||
| @Service | |||
| open class DoWorkbenchReleaseService( | |||
| private val deliveryOrderService: DeliveryOrderService, | |||
| private val doReplenishmentService: DoReplenishmentService, | |||
| private val jdbcDao: JdbcDao, | |||
| private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, | |||
| private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, | |||
| @@ -208,14 +209,21 @@ open class DoWorkbenchReleaseService( | |||
| println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}") | |||
| } | |||
| val replenishmentPickOrderIds = runWorkbenchReplenishmentRelease(successResults) | |||
| if (!useV2) { | |||
| successResults.forEach { result -> | |||
| val pickOrdersForDownstream = (successResults.map { it.pickOrderId } + replenishmentPickOrderIds).toSet() | |||
| pickOrdersForDownstream.forEach { pickOrderId -> | |||
| try { | |||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(result.pickOrderId) | |||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(result.pickOrderId, userId) | |||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(pickOrderId) | |||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId) | |||
| } catch (e: Exception) { | |||
| val deliveryOrderId = successResults.firstOrNull { it.pickOrderId == pickOrderId }?.deliveryOrderId | |||
| ?: 0L | |||
| synchronized(status.failed) { | |||
| status.failed.add(result.deliveryOrderId to ("Downstream workbench step failed: ${e.message}")) | |||
| status.failed.add( | |||
| deliveryOrderId to ("Downstream workbench step failed for pick order $pickOrderId: ${e.message}") | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| @@ -342,11 +350,13 @@ open class DoWorkbenchReleaseService( | |||
| } | |||
| val createdHeaders = createAndLinkDeliveryOrderPickOrders(successResults, "batch", mergeExtraIntoLaneTicket) | |||
| val replenishmentPickOrderIds = runWorkbenchReplenishmentRelease(successResults) | |||
| if (!useV2) { | |||
| successResults.forEach { result -> | |||
| val pickOrdersForDownstream = (successResults.map { it.pickOrderId } + replenishmentPickOrderIds).toSet() | |||
| pickOrdersForDownstream.forEach { pickOrderId -> | |||
| try { | |||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(result.pickOrderId) | |||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(result.pickOrderId, userId) | |||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(pickOrderId) | |||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId) | |||
| } catch (_: Exception) { | |||
| // keep batch release resilient; failures are reflected by missing downstream data and can be re-triggered | |||
| } | |||
| @@ -460,6 +470,16 @@ open class DoWorkbenchReleaseService( | |||
| } | |||
| } | |||
| private fun runWorkbenchReplenishmentRelease(successResults: List<ReleaseDoResult>): Set<Long> { | |||
| if (successResults.isEmpty()) return emptySet() | |||
| return try { | |||
| doReplenishmentService.releasePendingReplenishmentsForWorkbenchBatch(successResults) | |||
| } catch (e: Exception) { | |||
| println("❌ workbench replenishment release failed: ${e.message}") | |||
| emptySet() | |||
| } | |||
| } | |||
| private fun laneGroupKey(result: ReleaseDoResult): String = | |||
| listOf( | |||
| result.shopId?.toString() ?: "", | |||
| @@ -141,6 +141,14 @@ class DeliveryOrderController( | |||
| return doReplenishmentService.list(deliveryDate, status) | |||
| } | |||
| @GetMapping("/replenishment/for-batch-release") | |||
| fun listReplenishmentForBatchRelease( | |||
| @RequestParam(required = false) truckLaneCode: String?, | |||
| @RequestParam(required = false) shopName: String?, | |||
| ): List<DoReplenishmentResponse> { | |||
| return doReplenishmentService.listForBatchRelease(truckLaneCode, shopName) | |||
| } | |||
| @GetMapping("/search-code/{code}") | |||
| fun searchByCode(@PathVariable code: String): List<DeliveryOrderInfo> { | |||
| return deliveryOrderService.searchByCode(code); | |||
| @@ -48,6 +48,7 @@ data class DoReplenishmentResponse( | |||
| val targetDoId: Long?, | |||
| val targetDoCode: String?, | |||
| val pickOrderLineId: Long?, | |||
| val deliveryOrderPickOrderId: Long?, | |||
| val status: String, | |||
| @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") | |||
| val created: LocalDateTime?, | |||
| @@ -54,6 +54,30 @@ open class JoWorkbenchMainService( | |||
| private val workbenchDefaultExcludeWarehouseCodes: Set<String> = setOf( | |||
| //"2F-W202-01-00", | |||
| //"2F-W200-#A-00", | |||
| "4F-W402-01-00", | |||
| "4F-W402-02-00", | |||
| "4F-W402-03-00", | |||
| "4F-W402-04-00", | |||
| "4F-W402-05-00", | |||
| "4F-W402-#A-00", | |||
| "4F-W402-#B-00", | |||
| "4F-W402-#C-00", | |||
| "4F-W402-#D-00", | |||
| "4F-W402-#E-00", | |||
| "4F-W402-#F-00", | |||
| "4F-W402-#G-00", | |||
| "4F-W402-#H-00", | |||
| "4F-W402-#I-00", | |||
| "4F-W402-#J-00", | |||
| "4F-W402-#K-00", | |||
| "4F-W402-#L-00", | |||
| "4F-W402-#M-00", | |||
| "4F-W402-#N-00", | |||
| "4F-W402-#O-00", | |||
| "4F-W402-#P-00", | |||
| "4F-W402-#Q-00", | |||
| "4F-W402-#R-00", | |||
| "4F-W402-#S-00" | |||
| ) | |||
| private fun debugPrintSuggestionNullReasons(pickOrderId: Long) { | |||
| @@ -23,6 +23,7 @@ import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderService | |||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoReplenishmentService | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | |||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | |||
| @@ -80,6 +81,7 @@ private val inventoryLotLineService: InventoryLotLineService, | |||
| private val inventoryRepository: InventoryRepository, | |||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository, | |||
| private val itemUomService: ItemUomService, | |||
| @Lazy private val doReplenishmentService: DoReplenishmentService, | |||
| ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { | |||
| @PersistenceContext | |||
| @@ -102,6 +104,7 @@ private val inventoryLotLineService: InventoryLotLineService, | |||
| if (pol.status != PickOrderLineStatus.COMPLETED) { | |||
| pol.status = PickOrderLineStatus.COMPLETED | |||
| pickOrderLineRepository.save(pol) | |||
| doReplenishmentService.completeByPickOrderLineId(pickOrderLineId) | |||
| } | |||
| // Optionally bubble up to pick order completion (safe no-op if not ready) | |||
| @@ -0,0 +1,12 @@ | |||
| --liquibase formatted sql | |||
| -- Add column deliveryOrderPickOrderId to do_replenishment | |||
| --changeset Enson:20260612-01 | |||
| ALTER TABLE do_replenishment | |||
| ADD COLUMN deliveryOrderPickOrderId BIGINT DEFAULT NULL; | |||
| --changeset Enson:20260612-02 | |||
| CREATE INDEX idx_dr_target_do_id | |||
| ON do_replenishment (targetDoId); | |||
| --changeset Enson:20260612-03 | |||
| CREATE INDEX idx_dr_delivery_order_pick_order_id | |||
| ON do_replenishment (deliveryOrderPickOrderId); | |||