diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishment.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishment.kt index 427f11b..04fe50c 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishment.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishment.kt @@ -86,6 +86,9 @@ open class DoReplenishment : BaseEntity() { @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) diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishmentRepository.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishmentRepository.kt index be1a035..33c178d 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishmentRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishmentRepository.kt @@ -20,6 +20,11 @@ interface DoReplenishmentRepository : AbstractRepository fun findByTargetDoIdInAndDeletedIsFalse(targetDoIds: Collection): List + fun findFirstByPickOrderLineIdAndStatusAndDeletedIsFalse( + pickOrderLineId: Long, + status: String, + ): DoReplenishment? + @Query( """ SELECT r FROM DoReplenishment r @@ -34,6 +39,29 @@ interface DoReplenishmentRepository : AbstractRepository @Param("status") status: String?, ): List + @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 + @Query( """ SELECT r.code FROM DoReplenishment r diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index 65f684a..f26fccc 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -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>, - private val pickOrderLineIds: Set, - ) { - 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, + stockOutLinesByPickOrderLineId: Map>, + illById: Map, + itemsById: Map, + deliveryOrderCodeById: Map, + isExtraByDoId: Map, + headerTicketNo: String? = null, + headerIsMerge: Boolean = false, + ): MutableMap { + 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): 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() + 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 = emptyMap(), headerTicketNo: String? = null, headerIsMerge: Boolean = false, - replenishPdfIndex: ReplenishPdfIndex = ReplenishPdfIndex.EMPTY, + replenishPdfIndex: DoReplenishmentService.ReplenishPdfIndex = DoReplenishmentService.ReplenishPdfIndex.EMPTY, ): MutableMap { val field = mutableMapOf() val isExtra = isExtraDeliveryTicket( diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt index 7686094..f0431d8 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt @@ -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 { + 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): List { if (targetDoIds.isEmpty()) return emptyList() return doReplenishmentRepository.findByTargetDoIdInAndDeletedIsFalse(targetDoIds) } + data class ReplenishPdfIndex( + private val targetDoItemKeys: Set>, + private val pickOrderLineIds: Set, + ) { + 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): 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, + exportLines: List, + ): List { + 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, + ): Set { + if (releasedResults.isEmpty()) return emptySet() + + val pending = findPendingReplenishmentsForReleasedResults(releasedResults) + if (pending.isEmpty()) return emptySet() + + val affectedPickOrderIds = mutableSetOf() + 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, + ): List { + 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() + 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, 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, ) diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt index 6be74ad..fe07e97 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt @@ -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( + { 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"] = "總箱數:" diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt index f349b2c..a557e76 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt @@ -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): Set { + 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() ?: "", diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt index e39a06d..5d11e4f 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt @@ -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 { + return doReplenishmentService.listForBatchRelease(truckLaneCode, shopName) + } + @GetMapping("/search-code/{code}") fun searchByCode(@PathVariable code: String): List { return deliveryOrderService.searchByCode(code); diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoReplenishmentModels.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoReplenishmentModels.kt index c404163..052aa77 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoReplenishmentModels.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoReplenishmentModels.kt @@ -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?, diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt index 42ed0fd..f4ef72f 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt @@ -54,6 +54,30 @@ open class JoWorkbenchMainService( private val workbenchDefaultExcludeWarehouseCodes: Set = 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) { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 715e71f..03b76df 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -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(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) diff --git a/src/main/resources/db/changelog/changes/20260612_Enson/02_setting.sql b/src/main/resources/db/changelog/changes/20260612_Enson/02_setting.sql new file mode 100644 index 0000000..4819183 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260612_Enson/02_setting.sql @@ -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); \ No newline at end of file