Explorar el Código

update pick order complete logci and shortner Transactional

production
CANCERYS\kw093 hace 1 mes
padre
commit
1b1f23c283
Se han modificado 6 ficheros con 231 adiciones y 90 borrados
  1. +187
    -90
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  2. +6
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrder.kt
  3. +15
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt
  4. +13
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt
  5. +2
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  6. +8
    -0
      src/main/resources/db/changelog/changes/20260504_01_Enson/04_alter_stock_take.sql

+ 187
- 90
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt Ver fichero

@@ -89,6 +89,11 @@ import java.io.FileNotFoundException
import java.util.Locale
import com.ffii.fpsms.modules.deliveryOrder.service.DeliveryOrderService
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.support.TransactionSynchronization
import org.springframework.transaction.support.TransactionSynchronizationManager
import org.springframework.transaction.support.TransactionTemplate
/**
* DO workbench: pick execution and related operations (v1: [scanPick]).
*/
@@ -122,6 +127,7 @@ open class DoWorkbenchMainService(
private val joPickOrderRepository: JoPickOrderRepository,
private val printerService: PrinterService,
private val itemsRepository: ItemsRepository,
private val transactionManager: PlatformTransactionManager,
) {
@PersistenceContext
private lateinit var entityManager: EntityManager
@@ -589,83 +595,30 @@ val saveSolMs = lapMs()

val pickOrderId = pol.pickOrder?.id
val poType = pol.pickOrder?.type
var rebuildMs = 0L
var ensureMs = 0L
var polPartialMs = 0L
var postMs = 0L
val effectiveExcludeWarehouseCodes = when (poType) {
PickOrderType.JOB_ORDER -> request.excludeWarehouseCodes ?: emptyList()
else -> request.excludeWarehouseCodes // null → DO 走 default;有傳則整份取代 default
}
if (pickOrderId != null) {
if (hasExplicitQty) {
rebuildMs = measureTimeMillis {
if (explicitRemainder > BigDecimal.ZERO) {
suggestedPickLotWorkbenchService.setNoHoldSuggestionsForPickOrderLineNextSingleLot(
pickOrderLineId = polId,
targetQty = explicitRemainder,
storeId = request.storeId,
excludeInventoryLotLineId = scannedIll.id,
excludeWarehouseCodes = effectiveExcludeWarehouseCodes,
)
} else {
suggestedPickLotWorkbenchService.setNoHoldSuggestionsForPickOrderLineExactQty(
pickOrderLineId = polId,
targetQty = BigDecimal.ZERO,
excludeWarehouseCodes = effectiveExcludeWarehouseCodes,
)
}
}
ensureMs = measureTimeMillis {
if (explicitRemainder > BigDecimal.ZERO) {
stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderLineNoHold(polId, request.userId)
} else {
val allSolEntities =
stockOutLIneRepository.findAllByPickOrderLineIdInAndDeletedFalse(listOf(polId))
val toClose = allSolEntities.filter { it.id != sol.id && !isWorkbenchSolEndStatus(it.status) }
if (toClose.isNotEmpty()) {
toClose.forEach { s ->
s.status = StockOutLineStatus.COMPLETE.status
if (s.startTime == null) s.startTime = LocalDateTime.now()
s.endTime = LocalDateTime.now()
}
stockOutLIneRepository.saveAll(toClose)
stockOutLIneRepository.flush()
}
}
}
} else {
rebuildMs = measureTimeMillis {
suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrderLine(
pickOrderLineId = polId,
storeId = request.storeId,
excludeWarehouseCodes = effectiveExcludeWarehouseCodes,
)
}
ensureMs = measureTimeMillis {
stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderLineNoHold(polId, request.userId)
}
}

if (effectiveLotExhaustedSplit) {
polPartialMs = measureTimeMillis {
val polEntity = pickOrderLineRepository.findById(polId).orElse(null)
if (polEntity != null) {
polEntity.status = PickOrderLineStatus.PARTIALLY_COMPLETE
pickOrderLineRepository.save(polEntity)
}
}
val ledgerMs = measureTimeMillis { createWorkbenchPickLedger(sol, effectiveDelta) }
registerAfterCommit {
runInNewTransaction {
runWorkbenchPickDeferredFollowUps(
solId = sol.id!!,
polId = polId,
pickOrderId = pickOrderId,
itemId = itemId,
userId = request.userId,
hasExplicitQty = hasExplicitQty,
explicitRemainder = explicitRemainder,
scannedIllId = scannedIll.id,
requestStoreId = request.storeId,
effectiveExcludeWarehouseCodes = effectiveExcludeWarehouseCodes,
effectiveLotExhaustedSplit = effectiveLotExhaustedSplit,
effectiveDelta = effectiveDelta,
)
}

updateJoPickOrderHandledByIfJobOrder(
pickOrderId = pickOrderId,
itemId = itemId,
userId = request.userId,
)
}

postMs = measureTimeMillis { postWorkbenchPickSideEffects(sol, effectiveDelta) }

val mapFetchT0 = System.nanoTime()
val mapped = stockOutLIneRepository.findStockOutLineInfoById(sol.id!!)
val mapFetchMs = (System.nanoTime() - mapFetchT0) / 1_000_000
@@ -673,15 +626,12 @@ val mapFetchMs = (System.nanoTime() - mapFetchT0) / 1_000_000
val totalMs = (System.nanoTime() - wall0) / 1_000_000
/*
log.info(
"workbench scanPick timing (ms): total={} prep={} outbound={} saveSol={} rebuildSpl={} ensureSol={} polPartial={} postEffects={} mapFetch={} lotSplit={} solId={} polId={} poId={}",
"workbench scanPick timing (ms): total={} prep={} outbound={} saveSol={} ledger={} mapFetch={} lotSplit={} solId={} polId={} poId={}",
totalMs,
prepMs,
outboundMs,
saveSolMs,
rebuildMs,
ensureMs,
polPartialMs,
postMs,
ledgerMs,
mapFetchMs,
effectiveLotExhaustedSplit,
sol.id,
@@ -1952,16 +1902,152 @@ return MessageResponse(
}
checkWorkbenchPickOrderLineCompleted(polId, solsForPol)
val pickOrder = pol.pickOrder ?: return
val poId = pickOrder.id ?: return
val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(poId)
val allCompleted = allLines.isNotEmpty() && allLines.all { it.status == PickOrderLineStatus.COMPLETED }
val poId = pol.pickOrder?.id ?: return
val freshPo = pickOrderRepository.findById(poId).orElse(null) ?: return
val total = freshPo.totalLines ?: 0
val completed = freshPo.submittedLines ?: 0
val allCompleted = total > 0 && completed >= total
if (allCompleted) {
completePickOrderWithRetry(poId)
tryCompleteDeliveryOrderPickOrderTicketCompleted(poId)
}
}
private fun registerAfterCommit(action: () -> Unit) {
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
action()
return
}
TransactionSynchronizationManager.registerSynchronization(
object : TransactionSynchronization {
override fun afterCommit() {
action()
}
},
)
}

private fun runInNewTransaction(action: () -> Unit) {
val txTemplate = TransactionTemplate(transactionManager).apply {
propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
}
txTemplate.executeWithoutResult {
action()
}
}

private fun postWorkbenchPickSideEffects(savedStockOutLine: StockOutLine, deltaQty: BigDecimal) {
private fun runWorkbenchPickDeferredFollowUps(
solId: Long,
polId: Long,
pickOrderId: Long?,
itemId: Long,
userId: Long,
hasExplicitQty: Boolean,
explicitRemainder: BigDecimal,
scannedIllId: Long?,
requestStoreId: String?,
effectiveExcludeWarehouseCodes: List<String>?,
effectiveLotExhaustedSplit: Boolean,
effectiveDelta: BigDecimal,
) {
val deferredStart = System.nanoTime()
var rebuildMs = 0L
var ensureMs = 0L
var polPartialMs = 0L
var joHandledByMs = 0L
var postMs = 0L
try {
if (pickOrderId != null) {
if (hasExplicitQty) {
rebuildMs = measureTimeMillis {
if (explicitRemainder > BigDecimal.ZERO) {
suggestedPickLotWorkbenchService.setNoHoldSuggestionsForPickOrderLineNextSingleLot(
pickOrderLineId = polId,
targetQty = explicitRemainder,
storeId = requestStoreId,
excludeInventoryLotLineId = scannedIllId,
excludeWarehouseCodes = effectiveExcludeWarehouseCodes,
)
} else {
suggestedPickLotWorkbenchService.setNoHoldSuggestionsForPickOrderLineExactQty(
pickOrderLineId = polId,
targetQty = BigDecimal.ZERO,
excludeWarehouseCodes = effectiveExcludeWarehouseCodes,
)
}
}
ensureMs = measureTimeMillis {
if (explicitRemainder > BigDecimal.ZERO) {
stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderLineNoHold(polId, userId)
} else {
val allSolEntities =
stockOutLIneRepository.findAllByPickOrderLineIdInAndDeletedFalse(listOf(polId))
val toClose = allSolEntities.filter { it.id != solId && !isWorkbenchSolEndStatus(it.status) }
if (toClose.isNotEmpty()) {
toClose.forEach { s ->
s.status = StockOutLineStatus.COMPLETE.status
if (s.startTime == null) s.startTime = LocalDateTime.now()
s.endTime = LocalDateTime.now()
}
stockOutLIneRepository.saveAll(toClose)
stockOutLIneRepository.flush()
}
}
}
} else {
rebuildMs = measureTimeMillis {
suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrderLine(
pickOrderLineId = polId,
storeId = requestStoreId,
excludeWarehouseCodes = effectiveExcludeWarehouseCodes,
)
}
ensureMs = measureTimeMillis {
stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderLineNoHold(polId, userId)
}
}

if (effectiveLotExhaustedSplit) {
polPartialMs = measureTimeMillis {
val polEntity = pickOrderLineRepository.findById(polId).orElse(null)
if (polEntity != null) {
polEntity.status = PickOrderLineStatus.PARTIALLY_COMPLETE
pickOrderLineRepository.save(polEntity)
}
}
}

joHandledByMs = measureTimeMillis {
updateJoPickOrderHandledByIfJobOrder(
pickOrderId = pickOrderId,
itemId = itemId,
userId = userId,
)
}
}
val savedSol = stockOutLIneRepository.findById(solId).orElse(null)
if (savedSol != null) {
postMs = measureTimeMillis {
postWorkbenchPickSideEffects(savedSol, effectiveDelta, createLedger = false)
}
}
} catch (e: Exception) {
log.error("WORKBENCH_DEFERRED_PICK_FAILED solId={} polId={} msg={}", solId, polId, e.message, e)
} finally {
val totalMs = (System.nanoTime() - deferredStart) / 1_000_000
log.info(
"WORKBENCH_DEFERRED_PICK_TRACE solId={} polId={} totalMs={} rebuildMs={} ensureMs={} polPartialMs={} joHandledByMs={} postMs={}",
solId,
polId,
totalMs,
rebuildMs,
ensureMs,
polPartialMs,
joHandledByMs,
postMs,
)
}
}
private fun postWorkbenchPickSideEffects(savedStockOutLine: StockOutLine, deltaQty: BigDecimal, createLedger: Boolean = true) {
if (deltaQty <= BigDecimal.ZERO) return
val wall0 = System.nanoTime()
var ledgerMs = 0L
@@ -1971,7 +2057,9 @@ return MessageResponse(
var poLinesFetchMs = 0L
var poCompleteAndDoMs = 0L

ledgerMs = measureTimeMillis { createWorkbenchPickLedger(savedStockOutLine, deltaQty) }
if (createLedger) {
ledgerMs = measureTimeMillis { createWorkbenchPickLedger(savedStockOutLine, deltaQty) }
}
try {
val bagT0 = System.nanoTime()
val solItem = savedStockOutLine.item
@@ -2018,15 +2106,19 @@ return MessageResponse(
polCompleteMs = measureTimeMillis { tryCompletePickOrderLineWorkbench(polId, solsForPol) }
}
polCheckMs = measureTimeMillis { checkWorkbenchPickOrderLineCompleted(polId, solsForPol) }
val pickOrder = pol.pickOrder
if (pickOrder != null && pickOrder.id != null) {
val poId = pickOrder.id!!
val poId = pol.pickOrder?.id ?: return
val freshPo = pickOrderRepository.findById(poId).orElse(null) ?: return
if (freshPo != null && freshPo.id != null) {
val poId = freshPo.id!!
val t0 = System.nanoTime()
val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(poId)
//val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(poId)
poLinesFetchMs = (System.nanoTime() - t0) / 1_000_000
// Split rows use pick_order_line.status = partially_completed until fully picked; do not complete header PO until every line is completed.
val allCompleted = allLines.all { it.status == PickOrderLineStatus.COMPLETED }
if (allCompleted && allLines.isNotEmpty()) {
// val allCompleted = allLines.all { it.status == PickOrderLineStatus.COMPLETED }
val total = freshPo.totalLines ?: 0
val completed = freshPo.submittedLines ?: 0
val allCompleted = total > 0 && completed >= total
if (allCompleted) {
poCompleteAndDoMs = measureTimeMillis {
// Use reload+retry to avoid optimistic lock when other flows update the same PO.
completePickOrderWithRetry(poId)
@@ -2100,13 +2192,18 @@ return MessageResponse(
* Skips [checkAndCompletePickOrderByConsoCode] if POL was already completed (avoids full conso scan every pick).
*/
private fun tryCompletePickOrderLineWorkbench(pickOrderLineId: Long, sols: List<StockOutLineInfo>) {
val polEntity = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return
if (polEntity.status == PickOrderLineStatus.COMPLETED) return
if (sols.isEmpty()) return
val allEnded = sols.all { isWorkbenchSolEndStatus(it.status) }
if (!allEnded) return
polEntity.status = PickOrderLineStatus.COMPLETED
pickOrderLineRepository.save(polEntity)

val polEntity = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return
val poId = polEntity.pickOrder?.id ?: return

// Atomic gate: only one concurrent request can transition this POL to COMPLETED.
val changed = pickOrderLineRepository.markCompletedIfNotCompleted(pickOrderLineId)
if (changed > 0) {
pickOrderRepository.incrementSubmittedLines(poId)
}
}

/**


+ 6
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrder.kt Ver fichero

@@ -66,4 +66,10 @@ open class PickOrder: BaseEntity<Long>() {
@ManyToOne
@JoinColumn(name = "assignTo", referencedColumnName = "id")
open var assignTo: User? = null

@Column(name = "totalLines")
open var totalLines: Int? = null

@Column(name = "submittedLines")
open var submittedLines: Int? = null
}

+ 15
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderLineRepository.kt Ver fichero

@@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.pickOrder.entity

import com.ffii.core.support.AbstractRepository
import org.springframework.stereotype.Repository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
@Repository
@@ -31,4 +32,18 @@ fun findByPickOrderId(pickOrderId: Long): List<PickOrderLine>
"""
)
fun findAllByPickOrderIdInAndDeletedFalse(@Param("pickOrderIds") pickOrderIds: List<Long>): List<PickOrderLine>

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(
value = """
UPDATE fpsmsdb.pick_order_line
SET status = 'completed',
modified = CURRENT_TIMESTAMP
WHERE id = :pickOrderLineId
AND deleted = 0
AND LOWER(COALESCE(status, '')) <> 'completed'
""",
nativeQuery = true,
)
fun markCompletedIfNotCompleted(@Param("pickOrderLineId") pickOrderLineId: Long): Int
}

+ 13
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt Ver fichero

@@ -218,4 +218,17 @@ fun findAllReleasedJoWorkbenchPickOrders(
@Param("status") status: PickOrderStatus,
@Param("completedStatus") completedStatus: JobOrderStatus,
): List<PickOrder>

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(
value = """
UPDATE fpsmsdb.pick_order
SET submittedLines = COALESCE(submittedLines, 0) + 1,
modified = CURRENT_TIMESTAMP
WHERE id = :pickOrderId
AND deleted = 0
""",
nativeQuery = true,
)
fun incrementSubmittedLines(@Param("pickOrderId") pickOrderId: Long): Int
}

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Ver fichero

@@ -118,6 +118,8 @@ open class PickOrderService(
this.targetDate = request.targetDate.atStartOfDay()
this.type = request.type
this.status = PickOrderStatus.PENDING
this.totalLines = request.pickOrderLine.size
this.submittedLines = 0
}
val savedPickOrder = saveAndFlush(pickOrder)
val polEntries = request.pickOrderLine.map {


+ 8
- 0
src/main/resources/db/changelog/changes/20260504_01_Enson/04_alter_stock_take.sql Ver fichero

@@ -0,0 +1,8 @@
--liquibase formatted sql



--changeset Enson:20260507-01
ALTER TABLE fpsmsdb.pick_order
ADD COLUMN `totalLines` INT(11) after `deliveryOrderPickOrderId`,
ADD COLUMN `submittedLines` INT(11) after `totalLines`;

Cargando…
Cancelar
Guardar