Browse Source

補貨V1

production
CANCERYS\kw093 1 week ago
parent
commit
88e36de88c
11 changed files with 438 additions and 37 deletions
  1. +3
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishment.kt
  2. +28
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishmentRepository.kt
  3. +68
    -29
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  4. +232
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt
  5. +32
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  6. +27
    -7
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt
  7. +8
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt
  8. +1
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoReplenishmentModels.kt
  9. +24
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt
  10. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  11. +12
    -0
      src/main/resources/db/changelog/changes/20260612_Enson/02_setting.sql

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishment.kt View File

@@ -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)


+ 28
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DoReplenishmentRepository.kt View File

@@ -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


+ 68
- 29
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt View File

@@ -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(


+ 232
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoReplenishmentService.kt View File

@@ -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,
)


+ 32
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt View File

@@ -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"] = "總箱數:"


+ 27
- 7
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchReleaseService.kt View File

@@ -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() ?: "",


+ 8
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt View File

@@ -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);


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoReplenishmentModels.kt View File

@@ -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?,


+ 24
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt View File

@@ -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) {


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt View File

@@ -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)


+ 12
- 0
src/main/resources/db/changelog/changes/20260612_Enson/02_setting.sql View File

@@ -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);

Loading…
Cancel
Save