| @@ -86,6 +86,9 @@ open class DoReplenishment : BaseEntity<Long>() { | |||||
| @Column(name = "pickOrderLineId") | @Column(name = "pickOrderLineId") | ||||
| open var pickOrderLineId: Long? = null | open var pickOrderLineId: Long? = null | ||||
| @Column(name = "deliveryOrderPickOrderId") | |||||
| open var deliveryOrderPickOrderId: Long? = null | |||||
| @NotNull | @NotNull | ||||
| @Size(max = 20) | @Size(max = 20) | ||||
| @Column(name = "status", nullable = false, length = 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 findByTargetDoIdInAndDeletedIsFalse(targetDoIds: Collection<Long>): List<DoReplenishment> | ||||
| fun findFirstByPickOrderLineIdAndStatusAndDeletedIsFalse( | |||||
| pickOrderLineId: Long, | |||||
| status: String, | |||||
| ): DoReplenishment? | |||||
| @Query( | @Query( | ||||
| """ | """ | ||||
| SELECT r FROM DoReplenishment r | SELECT r FROM DoReplenishment r | ||||
| @@ -34,6 +39,29 @@ interface DoReplenishmentRepository : AbstractRepository<DoReplenishment, Long> | |||||
| @Param("status") status: String?, | @Param("status") status: String?, | ||||
| ): List<DoReplenishment> | ): 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( | @Query( | ||||
| """ | """ | ||||
| SELECT r.code FROM DoReplenishment r | SELECT r.code FROM DoReplenishment r | ||||
| @@ -33,6 +33,7 @@ import java.math.BigDecimal | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrder | 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.enums.DoPickOrderStatus | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.ExportDeliveryNoteRequest | import com.ffii.fpsms.modules.deliveryOrder.web.models.ExportDeliveryNoteRequest | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDeliveryNoteRequest | import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDeliveryNoteRequest | ||||
| @@ -125,7 +126,7 @@ open class DeliveryOrderService( | |||||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | ||||
| private val itemsRepository: ItemsRepository, | private val itemsRepository: ItemsRepository, | ||||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | ||||
| private val doReplenishmentService: DoReplenishmentService, | |||||
| @Lazy private val doReplenishmentService: DoReplenishmentService, | |||||
| ) { | ) { | ||||
| /** | /** | ||||
| * 樓層篩選:2F/4F/ALL 由 [DoFloorSupplierSettingsService] 讀 `settings`。 | * 樓層篩選:2F/4F/ALL 由 [DoFloorSupplierSettingsService] 讀 `settings`。 | ||||
| @@ -1272,7 +1273,7 @@ open class DeliveryOrderService( | |||||
| val deliveryOrders = deliveryOrderRepository.findAllById(deliveryOrderIds) | val deliveryOrders = deliveryOrderRepository.findAllById(deliveryOrderIds) | ||||
| val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") } | val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") } | ||||
| val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra } | val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra } | ||||
| val replenishPdfIndex = buildReplenishPdfIndex(deliveryOrderIds) | |||||
| val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(deliveryOrderIds) | |||||
| val exportLines = deliveryNoteExportLines(deliveryNoteInfo) | val exportLines = deliveryNoteExportLines(deliveryNoteInfo) | ||||
| val sortedLines = exportLines.sortedBy { row -> | val sortedLines = exportLines.sortedBy { row -> | ||||
| @@ -1459,7 +1460,7 @@ open class DeliveryOrderService( | |||||
| val deliveryOrders = deliveryOrderRepository.findAllById(deliveryOrderIds) | val deliveryOrders = deliveryOrderRepository.findAllById(deliveryOrderIds) | ||||
| val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") } | val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") } | ||||
| val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra } | val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra } | ||||
| val replenishPdfIndex = buildReplenishPdfIndex(deliveryOrderIds) | |||||
| val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(deliveryOrderIds) | |||||
| sortedLines.forEach { row -> | sortedLines.forEach { row -> | ||||
| fields.add( | 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["dnTitle"] = "送貨單" | ||||
| params["colQty"] = "已提數量" | params["colQty"] = "已提數量" | ||||
| @@ -1592,33 +1610,54 @@ open class DeliveryOrderService( | |||||
| return label | 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( | fun buildDeliveryNotePdfLineField( | ||||
| @@ -1634,7 +1673,7 @@ open class DeliveryOrderService( | |||||
| isExtraByDoId: Map<Long, Boolean> = emptyMap(), | isExtraByDoId: Map<Long, Boolean> = emptyMap(), | ||||
| headerTicketNo: String? = null, | headerTicketNo: String? = null, | ||||
| headerIsMerge: Boolean = false, | headerIsMerge: Boolean = false, | ||||
| replenishPdfIndex: ReplenishPdfIndex = ReplenishPdfIndex.EMPTY, | |||||
| replenishPdfIndex: DoReplenishmentService.ReplenishPdfIndex = DoReplenishmentService.ReplenishPdfIndex.EMPTY, | |||||
| ): MutableMap<String, Any> { | ): MutableMap<String, Any> { | ||||
| val field = mutableMapOf<String, Any>() | val field = mutableMapOf<String, Any>() | ||||
| val isExtra = isExtraDeliveryTicket( | val isExtra = isExtraDeliveryTicket( | ||||
| @@ -1,5 +1,6 @@ | |||||
| package com.ffii.fpsms.modules.deliveryOrder.service | 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.DoPickOrderRecordRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoReplenishment | 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.entity.DeliveryOrderRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus | 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.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.SubmitDoReplenishmentLineRequest | ||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.SubmitDoReplenishmentRequest | 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.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.stereotype.Service | ||||
| import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| @@ -26,6 +33,10 @@ open class DoReplenishmentService( | |||||
| private val doPickOrderRepository: DoPickOrderRepository, | private val doPickOrderRepository: DoPickOrderRepository, | ||||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | ||||
| private val uomConversionRepository: UomConversionRepository, | private val uomConversionRepository: UomConversionRepository, | ||||
| private val pickOrderRepository: PickOrderRepository, | |||||
| private val pickOrderLineRepository: PickOrderLineRepository, | |||||
| private val itemsRepository: ItemsRepository, | |||||
| private val jdbcDao: JdbcDao, | |||||
| ) { | ) { | ||||
| @Transactional | @Transactional | ||||
| @@ -110,11 +121,231 @@ open class DoReplenishmentService( | |||||
| return toResponses(rows) | 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> { | open fun findReplenishmentsByTargetDoIds(targetDoIds: Collection<Long>): List<DoReplenishment> { | ||||
| if (targetDoIds.isEmpty()) return emptyList() | if (targetDoIds.isEmpty()) return emptyList() | ||||
| return doReplenishmentRepository.findByTargetDoIdInAndDeletedIsFalse(targetDoIds) | 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 { | private fun nextCodeSequence(deliveryDate: LocalDate): Int { | ||||
| val prefix = codePrefix(deliveryDate) | val prefix = codePrefix(deliveryDate) | ||||
| val suffixPattern = Regex("^${Regex.escape(prefix)}(\\d+)$") | val suffixPattern = Regex("^${Regex.escape(prefix)}(\\d+)$") | ||||
| @@ -206,6 +437,7 @@ open class DoReplenishmentService( | |||||
| targetDoId = row.targetDoId, | targetDoId = row.targetDoId, | ||||
| targetDoCode = row.targetDoCode, | targetDoCode = row.targetDoCode, | ||||
| pickOrderLineId = row.pickOrderLineId, | pickOrderLineId = row.pickOrderLineId, | ||||
| deliveryOrderPickOrderId = row.deliveryOrderPickOrderId, | |||||
| status = row.status, | status = row.status, | ||||
| created = row.created, | 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.bag.web.model.CreateBagLotLineRequest | ||||
| import com.ffii.fpsms.modules.common.CodeGenerator | import com.ffii.fpsms.modules.common.CodeGenerator | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | 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.DoPickOrderLineRecord | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository | import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository | ||||
| @@ -130,6 +131,7 @@ open class DoWorkbenchMainService( | |||||
| private val printerService: PrinterService, | private val printerService: PrinterService, | ||||
| private val itemsRepository: ItemsRepository, | private val itemsRepository: ItemsRepository, | ||||
| private val transactionManager: PlatformTransactionManager, | private val transactionManager: PlatformTransactionManager, | ||||
| private val doReplenishmentService: DoReplenishmentService, | |||||
| ) { | ) { | ||||
| @PersistenceContext | @PersistenceContext | ||||
| private lateinit var entityManager: EntityManager | private lateinit var entityManager: EntityManager | ||||
| @@ -2370,6 +2372,7 @@ return MessageResponse( | |||||
| val changed = pickOrderLineRepository.markCompletedIfNotCompleted(pickOrderLineId) | val changed = pickOrderLineRepository.markCompletedIfNotCompleted(pickOrderLineId) | ||||
| if (changed > 0) { | if (changed > 0) { | ||||
| pickOrderRepository.incrementSubmittedLines(poId) | pickOrderRepository.incrementSubmittedLines(poId) | ||||
| doReplenishmentService.completeByPickOrderLineId(pickOrderLineId) | |||||
| } | } | ||||
| } | } | ||||
| @@ -2492,6 +2495,7 @@ return MessageResponse( | |||||
| pickOrderLineRepository.save( | pickOrderLineRepository.save( | ||||
| pol.apply { this.status = PickOrderLineStatus.COMPLETED }, | 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 isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra } | ||||
| val headerIsMerge = ctx.header.ticketNo?.startsWith("TI-M-") == true | val headerIsMerge = ctx.header.ticketNo?.startsWith("TI-M-") == true | ||||
| val headerTicketNo = ctx.header.ticketNo | val headerTicketNo = ctx.header.ticketNo | ||||
| val replenishPdfIndex = deliveryOrderService.buildReplenishPdfIndex(ctx.deliveryOrderIds) | |||||
| val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(ctx.deliveryOrderIds) | |||||
| sortedLines.forEach { row -> | sortedLines.forEach { row -> | ||||
| fields.add( | 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["dnTitle"] = "送貨單" | ||||
| params["colQty"] = "已提數量" | params["colQty"] = "已提數量" | ||||
| params["totalCartonTitle"] = "總箱數:" | params["totalCartonTitle"] = "總箱數:" | ||||
| @@ -90,6 +90,7 @@ data class WorkbenchBatchReleaseJobStatus( | |||||
| @Service | @Service | ||||
| open class DoWorkbenchReleaseService( | open class DoWorkbenchReleaseService( | ||||
| private val deliveryOrderService: DeliveryOrderService, | private val deliveryOrderService: DeliveryOrderService, | ||||
| private val doReplenishmentService: DoReplenishmentService, | |||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, | private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, | ||||
| private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, | private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, | ||||
| @@ -208,14 +209,21 @@ open class DoWorkbenchReleaseService( | |||||
| println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}") | println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}") | ||||
| } | } | ||||
| val replenishmentPickOrderIds = runWorkbenchReplenishmentRelease(successResults) | |||||
| if (!useV2) { | if (!useV2) { | ||||
| successResults.forEach { result -> | |||||
| val pickOrdersForDownstream = (successResults.map { it.pickOrderId } + replenishmentPickOrderIds).toSet() | |||||
| pickOrdersForDownstream.forEach { pickOrderId -> | |||||
| try { | try { | ||||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(result.pickOrderId) | |||||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(result.pickOrderId, userId) | |||||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(pickOrderId) | |||||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId) | |||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| val deliveryOrderId = successResults.firstOrNull { it.pickOrderId == pickOrderId }?.deliveryOrderId | |||||
| ?: 0L | |||||
| synchronized(status.failed) { | 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 createdHeaders = createAndLinkDeliveryOrderPickOrders(successResults, "batch", mergeExtraIntoLaneTicket) | ||||
| val replenishmentPickOrderIds = runWorkbenchReplenishmentRelease(successResults) | |||||
| if (!useV2) { | if (!useV2) { | ||||
| successResults.forEach { result -> | |||||
| val pickOrdersForDownstream = (successResults.map { it.pickOrderId } + replenishmentPickOrderIds).toSet() | |||||
| pickOrdersForDownstream.forEach { pickOrderId -> | |||||
| try { | try { | ||||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(result.pickOrderId) | |||||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(result.pickOrderId, userId) | |||||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(pickOrderId) | |||||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId) | |||||
| } catch (_: Exception) { | } catch (_: Exception) { | ||||
| // keep batch release resilient; failures are reflected by missing downstream data and can be re-triggered | // 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 = | private fun laneGroupKey(result: ReleaseDoResult): String = | ||||
| listOf( | listOf( | ||||
| result.shopId?.toString() ?: "", | result.shopId?.toString() ?: "", | ||||
| @@ -141,6 +141,14 @@ class DeliveryOrderController( | |||||
| return doReplenishmentService.list(deliveryDate, status) | 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}") | @GetMapping("/search-code/{code}") | ||||
| fun searchByCode(@PathVariable code: String): List<DeliveryOrderInfo> { | fun searchByCode(@PathVariable code: String): List<DeliveryOrderInfo> { | ||||
| return deliveryOrderService.searchByCode(code); | return deliveryOrderService.searchByCode(code); | ||||
| @@ -48,6 +48,7 @@ data class DoReplenishmentResponse( | |||||
| val targetDoId: Long?, | val targetDoId: Long?, | ||||
| val targetDoCode: String?, | val targetDoCode: String?, | ||||
| val pickOrderLineId: Long?, | val pickOrderLineId: Long?, | ||||
| val deliveryOrderPickOrderId: Long?, | |||||
| val status: String, | val status: String, | ||||
| @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") | @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") | ||||
| val created: LocalDateTime?, | val created: LocalDateTime?, | ||||
| @@ -54,6 +54,30 @@ open class JoWorkbenchMainService( | |||||
| private val workbenchDefaultExcludeWarehouseCodes: Set<String> = setOf( | private val workbenchDefaultExcludeWarehouseCodes: Set<String> = setOf( | ||||
| //"2F-W202-01-00", | //"2F-W202-01-00", | ||||
| //"2F-W200-#A-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) { | 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.enums.PickOrderStatus | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderService | 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.DoPickOrderRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository | ||||
| @@ -80,6 +81,7 @@ private val inventoryLotLineService: InventoryLotLineService, | |||||
| private val inventoryRepository: InventoryRepository, | private val inventoryRepository: InventoryRepository, | ||||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository, | private val pickExecutionIssueRepository: PickExecutionIssueRepository, | ||||
| private val itemUomService: ItemUomService, | private val itemUomService: ItemUomService, | ||||
| @Lazy private val doReplenishmentService: DoReplenishmentService, | |||||
| ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { | ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { | ||||
| @PersistenceContext | @PersistenceContext | ||||
| @@ -102,6 +104,7 @@ private val inventoryLotLineService: InventoryLotLineService, | |||||
| if (pol.status != PickOrderLineStatus.COMPLETED) { | if (pol.status != PickOrderLineStatus.COMPLETED) { | ||||
| pol.status = PickOrderLineStatus.COMPLETED | pol.status = PickOrderLineStatus.COMPLETED | ||||
| pickOrderLineRepository.save(pol) | pickOrderLineRepository.save(pol) | ||||
| doReplenishmentService.completeByPickOrderLineId(pickOrderLineId) | |||||
| } | } | ||||
| // Optionally bubble up to pick order completion (safe no-op if not ready) | // 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); | |||||