CANCERYS\kw093 6 часов назад
Родитель
Сommit
88067788ae
6 измененных файлов: 317 добавлений и 287 удалений
  1. +6
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt
  2. +4
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt
  3. +0
    -3
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
  4. +283
    -256
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  5. +21
    -27
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  6. +3
    -1
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/models/DoDetailResponse.kt Просмотреть файл

@@ -107,4 +107,10 @@ data class ReleasedDoPickOrderListItem(
data class AssignByDoPickOrderIdRequest(
val userId: Long,
val doPickOrderId: Long
)

/** Workbench: assign a `delivery_order_pick_order` ticket + its linked pick orders. */
data class AssignByDeliveryOrderPickOrderIdRequest(
val userId: Long,
val deliveryOrderPickOrderId: Long,
)

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt Просмотреть файл

@@ -12,5 +12,9 @@ interface SuggestPickLotRepository : AbstractRepository<SuggestedPickLot, Long>
fun findAllByPickOrderLineId(pickOrderLineId: Long): List<SuggestedPickLot>

fun findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId: Long): List<SuggestedPickLot>

fun findAllByPickOrderLineIdInAndDeletedFalse(pickOrderLineIds: List<Long>): List<SuggestedPickLot>

fun findFirstByStockOutLineId(stockOutLineId: Long): SuggestedPickLot?
}

+ 0
- 3
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt Просмотреть файл

@@ -115,9 +115,6 @@ open class InventoryLotLineService(
val stockUom = request.stockUomId?.let { itemUomRespository.findById(it).getOrNull() }
val status = request.status?.let { _status -> InventoryLotLineStatus.entries.find { it.value == _status } }

println("status: ${request.status}")
println("status123: ${status?.value}")

inventoryLotLine.apply {
this.inventoryLot = inventoryLot
this.warehouse = warehouse


+ 283
- 256
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Просмотреть файл

@@ -46,6 +46,9 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository
import java.time.LocalTime
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext
import java.util.UUID
@Service
open class StockOutLineService(
@@ -75,6 +78,10 @@ private val inventoryLotLineService: InventoryLotLineService,
private val pickExecutionIssueRepository: PickExecutionIssueRepository,
private val itemUomService: ItemUomService,
): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) {

@PersistenceContext
private lateinit var entityManager: EntityManager

private fun isEndStatus(status: String?): Boolean {
val s = status?.trim()?.lowercase() ?: return false
return s == "completed" || s == "rejected" || s == "partially_completed"
@@ -104,6 +111,22 @@ private val inventoryLotLineService: InventoryLotLineService,
}
}
}

/** When every POL on this pick order is COMPLETED or PARTIALLY_COMPLETE, mark pick order completed and cascade DO completion. */
private fun refreshPickOrderHeaderIfAllLinesCompleted(pickOrderId: Long) {
val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: return
val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId)
val allCompleted = allLines.all {
it.status == PickOrderLineStatus.COMPLETED || it.status == PickOrderLineStatus.PARTIALLY_COMPLETE
}
if (allCompleted && allLines.isNotEmpty()) {
pickOrder.status = PickOrderStatus.COMPLETED
pickOrderRepository.save(pickOrder)
completeDoForPickOrder(pickOrderId)
completeDoIfAllPickOrdersCompleted(pickOrderId)
}
}

@Throws(IOException::class)
@Transactional
open fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> {
@@ -378,7 +401,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
// })
// }
@Transactional
fun checkIsStockOutLineCompleted(pickOrderLineId: Long) {
fun checkIsStockOutLineCompleted(pickOrderLineId: Long, quiet: Boolean = false) {
val allStockOutLines = stockOutLineRepository
.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
@@ -409,7 +432,9 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
acc + (issue.issueQty ?: BigDecimal.ZERO)
}
} catch (e: Exception) {
println(" Error fetching issues for pickOrderLineId $pickOrderLineId: ${e.message}")
if (!quiet) {
println(" Error fetching issues for pickOrderLineId $pickOrderLineId: ${e.message}")
}
BigDecimal.ZERO
}

@@ -430,14 +455,16 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
// ✅ 现在的规则:这三类状态都算“已结束”
!(isComplete || isRejected || isPartiallyComplete)
}
println("Unfinished lines: ${unfinishedLine.size}")
if (unfinishedLine.isNotEmpty()) {
unfinishedLine.forEach { sol ->
println(" - StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}")

if (!quiet) {
println("Unfinished lines: ${unfinishedLine.size}")
if (unfinishedLine.isNotEmpty()) {
unfinishedLine.forEach { sol ->
println(" - StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}")
}
}
}
if (unfinishedLine.isEmpty()) {
// set pick order line status to complete
val pol = pickOrderLineRepository.findById(pickOrderLineId).orElseThrow()
@@ -447,11 +474,28 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
this.status = PickOrderLineStatus.COMPLETED
}
)
println("✅ Updated pick order line $pickOrderLineId from $previousStatus to COMPLETED")
} else {
if (!quiet) {
println("✅ Updated pick order line $pickOrderLineId from $previousStatus to COMPLETED")
}
} else if (!quiet) {
println("⏳ Pick order line $pickOrderLineId not completed yet - has ${unfinishedLine.size} unfinished stock out lines")
}
}

/** Batch pick: same qty rules as [InventoryLotLineService.updateInventoryLotLineQuantities] pick, without double findById / extra service layers. */
private fun applyPickToInventoryLotLineInBatch(ill: InventoryLotLine, submitQty: BigDecimal) {
val zero = BigDecimal.ZERO
val newHold = (ill.holdQty ?: zero).minus(submitQty)
val newOut = (ill.outQty ?: zero).plus(submitQty)
if (newHold < zero || newOut < zero) {
throw IllegalArgumentException("Invalid pick quantities for lotLine ${ill.id}: holdQty=$newHold, outQty=$newOut")
}
val prevStatus = ill.status
ill.holdQty = newHold
ill.outQty = newOut
ill.status = inventoryLotLineService.deriveInventoryLotLineStatus(prevStatus, ill.inQty, ill.outQty, ill.holdQty)
inventoryLotLineRepository.save(ill)
}
private fun completeDoIfAllPickOrdersCompleted(pickOrderId: Long) {
// 1) 先用 line 关联找 do_pick_order_id
val lines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(pickOrderId)
@@ -634,60 +678,66 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
println("Updating StockOutLine ID: ${request.id}")
println("Current status: ${stockOutLine.status}")
println("New status: ${request.status}")
val deferAggregate = request.deferAggregatePickOrderEffects == true
val savedStockOutLine = applyStockOutLineDelta(
stockOutLineId = request.id,
stockOutLine = stockOutLine,
deltaQty = BigDecimal((request.qty ?: 0.0).toString()),
newStatus = request.status,
skipInventoryWrite = request.skipInventoryWrite == true,
skipLedgerWrite = request.skipLedgerWrite == true
skipLedgerWrite = request.skipLedgerWrite == true,
skipTryCompletePickOrderLine = deferAggregate,
deferPersistenceFlush = deferAggregate
)
println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}")
try {
val item = savedStockOutLine.item
val inventoryLotLine = savedStockOutLine.inventoryLotLine
val reqDeltaQty = request.qty ?: 0.0
// 只在状态为 completed 或 partially_completed,且数量增加时创建 BagLotLine
val isCompletedOrPartiallyCompleted = request.status == "completed" ||
request.status == "partially_completed" ||
request.status == "PARTIALLY_COMPLETE"
if (item?.isBag == true &&
inventoryLotLine != null &&
isCompletedOrPartiallyCompleted &&
reqDeltaQty > 0) {
println(" Item isBag=true, creating BagLotLine...")
val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!)
if (bag != null) {
val lotNo = inventoryLotLine.inventoryLot?.lotNo
if (lotNo != null) {
val createBagLotLineRequest = CreateBagLotLineRequest(
bagId = bag.id!!,
lotId = inventoryLotLine.inventoryLot?.id ?: 0L,
itemId = item.id!!,
lotNo = lotNo,
stockQty = reqDeltaQty.toInt(),
date = LocalDate.now(),
time = LocalTime.now(),
stockOutLineId = savedStockOutLine.id
)

bagService.createBagLotLinesByBagId(createBagLotLineRequest)
println(" ✓ BagLotLine created successfully for item ${item.code}")
if (!deferAggregate) {
try {
val item = savedStockOutLine.item
val inventoryLotLine = savedStockOutLine.inventoryLotLine
val reqDeltaQty = request.qty ?: 0.0

// 只在状态为 completed 或 partially_completed,且数量增加时创建 BagLotLine
val isCompletedOrPartiallyCompleted = request.status == "completed" ||
request.status == "partially_completed" ||
request.status == "PARTIALLY_COMPLETE"

if (item?.isBag == true &&
inventoryLotLine != null &&
isCompletedOrPartiallyCompleted &&
reqDeltaQty > 0
) {

println(" Item isBag=true, creating BagLotLine...")

val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!)

if (bag != null) {
val lotNo = inventoryLotLine.inventoryLot?.lotNo
if (lotNo != null) {
val createBagLotLineRequest = CreateBagLotLineRequest(
bagId = bag.id!!,
lotId = inventoryLotLine.inventoryLot?.id ?: 0L,
itemId = item.id!!,
lotNo = lotNo,
stockQty = reqDeltaQty.toInt(),
date = LocalDate.now(),
time = LocalTime.now(),
stockOutLineId = savedStockOutLine.id
)

bagService.createBagLotLinesByBagId(createBagLotLineRequest)
println(" ✓ BagLotLine created successfully for item ${item.code}")
} else {
println(" Warning: lotNo is null, skipping BagLotLine creation")
}
} else {
println(" Warning: lotNo is null, skipping BagLotLine creation")
println(" Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation")
}
} else {
println(" Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation")
}
} catch (e: Exception) {
println(" Error creating BagLotLine: ${e.message}")
e.printStackTrace()
// 不中断主流程,只记录错误
}
} catch (e: Exception) {
println(" Error creating BagLotLine: ${e.message}")
e.printStackTrace()
// 不中断主流程,只记录错误
}
// 3. 如果被拒绝,触发特殊处理
if (request.status == "rejected" || request.status == "REJECTED") {
@@ -695,26 +745,18 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
handleLotRejectionFromStockOutLine(savedStockOutLine)
}
// 4. 自动刷 pickOrderLine 状态
val pickOrderLine = savedStockOutLine.pickOrderLine
if (pickOrderLine != null) {
checkIsStockOutLineCompleted(pickOrderLine.id)
// 5. 自动刷 pickOrder 状态
val pickOrder = pickOrderLine.pickOrder
if (pickOrder != null && pickOrder.id != null) {
// ✅ 修复:使用 repository 查询所有 lines,避免懒加载问题
val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrder.id!!)
val allCompleted = allLines.all { it.status == PickOrderLineStatus.COMPLETED || it.status == PickOrderLineStatus.PARTIALLY_COMPLETE }
if (allCompleted && allLines.isNotEmpty()) {
pickOrder.status = PickOrderStatus.COMPLETED
pickOrderRepository.save(pickOrder)
completeDoForPickOrder(pickOrder.id!!)
completeDoIfAllPickOrdersCompleted(pickOrder.id!!)
}
// 4–5. 自动刷 pickOrderLine / pickOrder 状态(批次提交在结尾统一处理)
if (!deferAggregate) {
val pickOrderLine = savedStockOutLine.pickOrderLine
if (pickOrderLine != null) {
checkIsStockOutLineCompleted(pickOrderLine.id)
pickOrderLine.pickOrder?.id?.let { refreshPickOrderHeaderIfAllLinesCompleted(it) }
}
}
val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)

val mappedSavedStockOutLine =
if (deferAggregate) null
else stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)
return MessageResponse(
id = savedStockOutLine.id,
name = savedStockOutLine.inventoryLotLine?.inventoryLot?.lotNo?: "",
@@ -1213,140 +1255,120 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
// 先前預載的 inventories 從未使用(save 已註解),已移除以避免批量提交無故失敗。
val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId }
println("Loading ${lotLineIds.size} lot lines...")
val lotLines = if (lotLineIds.isNotEmpty()) {
inventoryLotLineRepository.findAllById(lotLineIds).associateBy { it.id }
val lotLinesById: MutableMap<Long, InventoryLotLine> = if (lotLineIds.isNotEmpty()) {
inventoryLotLineRepository.findAllById(lotLineIds).associateByTo(mutableMapOf()) { it.id!! }
} else {
emptyMap()
mutableMapOf()
}

// 2) Bulk load all stock out lines to get current quantities
// 2) Bulk load all stock out lines(批次內就地更新)
val stockOutLineIds = request.lines.map { it.stockOutLineId }
println("Loading ${stockOutLineIds.size} stock out lines...")
val stockOutLines = stockOutLineRepository.findAllById(stockOutLineIds).associateBy { it.id }
val stockOutLinesById =
stockOutLineRepository.findAllById(stockOutLineIds).associateByTo(mutableMapOf()) { it.id!! }

// 3) Process each request line
// 3) Process each request line(直接 applyStockOutLineDelta + 內聯扣庫存,避免 updateStatus 與雙重查詢)
request.lines.forEach { line: QrPickSubmitLineRequest ->
val lineTrace = "$traceId|SOL=${line.stockOutLineId}"
try {
println("[$lineTrace] Processing line, noLot=${line.noLot}")
val solEntity = stockOutLinesById[line.stockOutLineId]
?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found")

if (line.noLot) {
// noLot branch
updateStatus(UpdateStockOutLineStatusRequest(
id = line.stockOutLineId,
status = "completed",
qty = 0.0
))
val updated = applyStockOutLineDelta(
stockOutLine = solEntity,
deltaQty = BigDecimal.ZERO,
newStatus = "completed",
skipInventoryWrite = true,
skipLedgerWrite = true,
skipTryCompletePickOrderLine = true,
deferPersistenceFlush = true
)
stockOutLinesById[line.stockOutLineId] = updated
processedIds += line.stockOutLineId
println("[$lineTrace] noLot processed (status->completed, qty=0)")
return@forEach
}

// 修复:从数据库获取当前实际数量
val stockOutLine = stockOutLines[line.stockOutLineId]
?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found")
val currentStatus = stockOutLine.status?.trim()?.lowercase()
val currentStatus = solEntity.status?.trim()?.lowercase()
if (currentStatus == "completed" || currentStatus == "complete") {
println("[$lineTrace] Skip because current status is already completed")
return@forEach
}
val currentActual = (stockOutLine.qty ?: 0.0).toBigDecimal()
val targetActual = line.actualPickQty ?: BigDecimal.ZERO
val required = line.requiredQty ?: BigDecimal.ZERO
println("[$lineTrace] currentActual=$currentActual, targetActual=$targetActual, required=$required")
// 计算增量(前端发送的是目标累计值)
val submitQty = targetActual - currentActual
println("[$lineTrace] submitQty(increment)=$submitQty")
// 使用前端发送的状态,否则根据数量自动判断
val newStatus = line.stockOutLineStatus
?: if (targetActual >= required) "completed" else "partially_completed"
if (submitQty <= BigDecimal.ZERO) {
println("[$lineTrace] submitQty<=0, only update status, skip inventory+ledger")
updateStatus(
UpdateStockOutLineStatusRequest(
id = line.stockOutLineId,
status = newStatus, // 例如前端传来的 "completed"
qty = 0.0, // 不改变现有 qty
skipLedgerWrite = true,
skipInventoryWrite = true
)
)
// 直接跳过后面的库存扣减逻辑
return@forEach

val currentActual = (solEntity.qty ?: 0.0).toBigDecimal()
val targetActual = line.actualPickQty ?: BigDecimal.ZERO
val required = line.requiredQty ?: BigDecimal.ZERO
val submitQty = targetActual - currentActual
val newStatus = line.stockOutLineStatus
?: if (targetActual >= required) "completed" else "partially_completed"

if (submitQty <= BigDecimal.ZERO) {
val updated = applyStockOutLineDelta(
stockOutLine = solEntity,
deltaQty = BigDecimal.ZERO,
newStatus = newStatus,
skipInventoryWrite = true,
skipLedgerWrite = true,
skipTryCompletePickOrderLine = true,
deferPersistenceFlush = true
)
stockOutLinesById[line.stockOutLineId] = updated
return@forEach
}

val savedSol = applyStockOutLineDelta(
stockOutLine = solEntity,
deltaQty = submitQty,
newStatus = newStatus,
skipInventoryWrite = true,
skipLedgerWrite = true,
skipTryCompletePickOrderLine = true,
deferPersistenceFlush = true
)
stockOutLinesById[line.stockOutLineId] = savedSol

val actualInventoryLotLineId =
line.inventoryLotLineId ?: savedSol.inventoryLotLine?.id

if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) {
val ill = lotLinesById[actualInventoryLotLineId]
?: inventoryLotLineRepository.findById(actualInventoryLotLineId).orElseThrow().also {
lotLinesById[actualInventoryLotLineId] = it
}
// 只有 submitQty > 0 时,才真正增加 qty 并触发库存扣减
updateStatus(
UpdateStockOutLineStatusRequest(
id = line.stockOutLineId,
status = newStatus,
qty = submitQty.toDouble(),
skipLedgerWrite = true,
skipInventoryWrite = true
)
val item = savedSol.item
val inventoryBeforeUpdate = item?.id?.let { itemId ->
itemUomService.findInventoryForItemBaseUom(itemId)
}
val onHandQtyBeforeUpdate =
(inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
applyPickToInventoryLotLineInBatch(ill, submitQty)
createStockLedgerForPickDelta(
stockOutLine = savedSol,
deltaQty = submitQty,
onHandQtyBeforeUpdate = onHandQtyBeforeUpdate,
traceTag = lineTrace,
flushAfterSave = false
)
println("[$lineTrace] stock_out_line qty/status updated with delta=$submitQty (inventory+ledger deferred)")
} else if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId == null) {
val item = savedSol.item
val inventoryBeforeUpdate = item?.id?.let { itemId ->
itemUomService.findInventoryForItemBaseUom(itemId)
}
val onHandQtyBeforeUpdate =
(inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
createStockLedgerForPickDelta(
stockOutLine = savedSol,
deltaQty = submitQty,
onHandQtyBeforeUpdate = onHandQtyBeforeUpdate,
traceTag = lineTrace,
flushAfterSave = false
)
}

// Inventory updates - 修复:使用增量数量
// ✅ 修复:如果 inventoryLotLineId 为 null,从 stock_out_line 中获取
val actualInventoryLotLineId = line.inventoryLotLineId
?: stockOutLine.inventoryLotLine?.id
// 在 newBatchSubmit 方法中,修改这部分代码(大约在 1169-1185 行)
if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) {
println("[$lineTrace] Updating inventory lot line $actualInventoryLotLineId with qty=$submitQty")
// ✅ 修复:在更新 inventory_lot_line 之前获取 inventory 的当前 onHandQty
val item = stockOutLine.item
val inventoryBeforeUpdate = item?.id?.let { itemId ->
itemUomService.findInventoryForItemBaseUom(itemId)
}
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
println("[$lineTrace] Inventory before update: onHandQty=$onHandQtyBeforeUpdate")
inventoryLotLineService.updateInventoryLotLineQuantities(
UpdateInventoryLotLineQuantitiesRequest(
inventoryLotLineId = actualInventoryLotLineId,
qty = submitQty,
operation = "pick"
)
)
if (submitQty > BigDecimal.ZERO) {
// ✅ 修复:传入更新前的 onHandQty,让 createStockLedgerForPickDelta 使用它
createStockLedgerForPickDelta(line.stockOutLineId, submitQty, onHandQtyBeforeUpdate, lineTrace)
}
} else if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId == null) {
// ✅ 修复:即使没有 inventoryLotLineId,也应该获取 inventory.onHandQty
val item = stockOutLine.item
val inventoryBeforeUpdate = item?.id?.let { itemId ->
itemUomService.findInventoryForItemBaseUom(itemId)
}
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
println("[$lineTrace] Warning: No inventoryLotLineId, still trying ledger creation")
createStockLedgerForPickDelta(line.stockOutLineId, submitQty, onHandQtyBeforeUpdate, lineTrace)
}
try {
val stockOutLine = stockOutLines[line.stockOutLineId]
val item = stockOutLine?.item
val inventoryLotLine = line.inventoryLotLineId?.let { lotLines[it] }
val item = savedSol.item
val inventoryLotLine = line.inventoryLotLineId?.let { lid -> lotLinesById[lid] }
if (item?.isBag == true && inventoryLotLine != null && submitQty > BigDecimal.ZERO) {
println(" Item isBag=true, creating BagLotLine...")
// 根据 itemId 查找对应的 Bag
val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!)
if (bag != null) {
val lotNo = inventoryLotLine.inventoryLot?.lotNo
if (lotNo != null) {
@@ -1354,29 +1376,21 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) {
bagId = bag.id!!,
lotId = inventoryLotLine.inventoryLot?.id ?: 0L,
itemId = item.id!!,
stockOutLineId = stockOutLine.id ,
stockOutLineId = savedSol.id,
lotNo = lotNo,
stockQty = submitQty.toInt(), // 转换为 Int
stockQty = submitQty.toInt(),
date = LocalDate.now(),
time = LocalTime.now()
)

bagService.createBagLotLinesByBagId(createBagLotLineRequest)
println(" ✓ BagLotLine created successfully for item ${item.code}")
} else {
println(" Warning: lotNo is null, skipping BagLotLine creation")
}
} else {
println(" Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation")
}
}
} catch (e: Exception) {
println(" Error creating BagLotLine: ${e.message}")
e.printStackTrace()
// 不中断主流程,只记录错误
}
processedIds += line.stockOutLineId
println("[$lineTrace] Line processed successfully")
} catch (e: Exception) {
println("[$lineTrace] Error processing line: ${e.message}")
e.printStackTrace()
@@ -1384,39 +1398,39 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) {
}
}

// 4) 移除:不需要保存 lotLines,因为它们没有被修改
// inventoryLotLineRepository.saveAll(lotLines.values.toList())
entityManager.flush()

// ✅ 修复:批处理完成后,检查所有受影响的 pick order lines 是否应该标记为完成
val affectedPickOrderIds = request.lines
.mapNotNull { line ->
stockOutLines[line.stockOutLineId]?.pickOrderLine?.pickOrder?.id
}
.distinct()
// 批次內已 defer:此處只處理本批有碰到的 POL,再刷新所屬 pick order 表頭,最後每個 conso 只掃一次
val polIdsTouched = request.lines.mapNotNull { line ->
stockOutLinesById[line.stockOutLineId]?.pickOrderLine?.id
}.distinct()

val allPickOrderLineIdsToCheck = if (affectedPickOrderIds.isNotEmpty()) {
affectedPickOrderIds.flatMap { pickOrderId ->
pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId).mapNotNull { it.id }
}.distinct()
} else {
emptyList()
println("=== Checking ${polIdsTouched.size} pick order lines touched by batch ===")
polIdsTouched.forEach { pickOrderLineId ->
try {
checkIsStockOutLineCompleted(pickOrderLineId, quiet = true)
} catch (e: Exception) {
println("Error checking pick order line $pickOrderLineId: ${e.message}")
}
}

println("=== Checking ${allPickOrderLineIdsToCheck.size} pick order lines (all lines of affected pick orders) after batch submit ===")
allPickOrderLineIdsToCheck.forEach { pickOrderLineId ->
val pickOrderIdsTouched = request.lines.mapNotNull { line ->
stockOutLinesById[line.stockOutLineId]?.pickOrderLine?.pickOrder?.id
}.distinct()

pickOrderIdsTouched.forEach { pickOrderId ->
try {
checkIsStockOutLineCompleted(pickOrderLineId)
refreshPickOrderHeaderIfAllLinesCompleted(pickOrderId)
} catch (e: Exception) {
println("Error checking pick order line $pickOrderLineId: ${e.message}")
println("Error refreshing pick order header for pickOrderId=$pickOrderId: ${e.message}")
}
}
val affectedConsoCodes = affectedPickOrderIds
.mapNotNull { pickOrderId ->
val po = pickOrderRepository.findById(pickOrderId).orElse(null)
po?.consoCode
}
.filter { !it.isNullOrBlank() }
.distinct()

val affectedConsoCodes = pickOrderIdsTouched.mapNotNull { pickOrderId ->
pickOrderRepository.findById(pickOrderId).orElse(null)?.consoCode
}
.filter { !it.isNullOrBlank() }
.distinct()

println("=== Checking completion by consoCode for ${affectedConsoCodes.size} affected consoCodes after batch submit ===")
affectedConsoCodes.forEach { consoCode ->
@@ -1559,63 +1573,72 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) {


private fun createStockLedgerForPickDelta(
stockOutLineId: Long,
stockOutLine: StockOutLine,
deltaQty: BigDecimal,
onHandQtyBeforeUpdate: Double? = null, // ✅ 新增参数:更新前的 onHandQty
traceTag: String? = null
onHandQtyBeforeUpdate: Double? = null,
traceTag: String? = null,
flushAfterSave: Boolean = true
) {
val tracePrefix = traceTag?.let { "[$it] " } ?: "[SOL=$stockOutLineId] "
val solId = stockOutLine.id
val tracePrefix = traceTag?.let { "[$it] " } ?: "[SOL=$solId] "
if (deltaQty <= BigDecimal.ZERO) {
println("${tracePrefix}Skip ledger creation: deltaQty <= 0 (deltaQty=$deltaQty)")
return
}
val sol = stockOutLineRepository.findById(stockOutLineId).orElse(null)
if (sol == null) {
println("${tracePrefix}Skip ledger creation: stockOutLine not found")
if (flushAfterSave) {
println("${tracePrefix}Skip ledger creation: deltaQty <= 0 (deltaQty=$deltaQty)")
}
return
}
val item = sol.item

val item = stockOutLine.item
if (item == null) {
println("${tracePrefix}Skip ledger creation: stockOutLine.item is null")
if (flushAfterSave) {
println("${tracePrefix}Skip ledger creation: stockOutLine.item is null")
}
return
}
val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!)
if (inventory == null) {
println("${tracePrefix}Skip ledger creation: inventory not found by itemId=${item.id}")
if (flushAfterSave) {
println("${tracePrefix}Skip ledger creation: inventory not found by itemId=${item.id}")
}
return
}
val previousBalance = resolvePreviousBalance(
itemId = item.id!!,
inventory = inventory,
onHandQtyBeforeUpdate = onHandQtyBeforeUpdate
)
val newBalance = previousBalance - deltaQty.toDouble()
println("${tracePrefix}Creating ledger: previousBalance=$previousBalance, deltaQty=$deltaQty, newBalance=$newBalance, inventoryId=${inventory.id}")
if (onHandQtyBeforeUpdate != null) {
println("${tracePrefix}Using onHandQtyBeforeUpdate: $onHandQtyBeforeUpdate")

if (flushAfterSave) {
println("${tracePrefix}Creating ledger: previousBalance=$previousBalance, deltaQty=$deltaQty, newBalance=$newBalance, inventoryId=${inventory.id}")
if (onHandQtyBeforeUpdate != null) {
println("${tracePrefix}Using onHandQtyBeforeUpdate: $onHandQtyBeforeUpdate")
}
}
val ledger = StockLedger().apply {
this.stockOutLine = sol
this.stockOutLine = stockOutLine
this.inventory = inventory
this.inQty = null
this.outQty = deltaQty.toDouble()
this.outQty = deltaQty.toDouble()
this.balance = newBalance
this.type = "NOR"
this.type = "NOR"
this.itemId = item.id
this.itemCode = item.code
this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
?: inventory.uom?.id
this.date = LocalDate.now()
}
stockLedgerRepository.saveAndFlush(ledger)
println("${tracePrefix}Ledger created successfully for stockOutLineId=$stockOutLineId")

if (flushAfterSave) {
stockLedgerRepository.saveAndFlush(ledger)
println("${tracePrefix}Ledger created successfully for stockOutLineId=$solId")
} else {
stockLedgerRepository.save(ledger)
}
}

private fun resolvePreviousBalance(
@@ -1886,20 +1909,20 @@ open fun batchScan(request: com.ffii.fpsms.modules.stock.web.model.BatchScanRequ
}
@Transactional
fun applyStockOutLineDelta(
stockOutLineId: Long,
stockOutLine: StockOutLine,
deltaQty: BigDecimal,
newStatus: String?,
typeOverride: String? = null,
skipInventoryWrite: Boolean = false,
skipLedgerWrite: Boolean = false,
operator: String? = null,
eventTime: LocalDateTime = LocalDateTime.now()
eventTime: LocalDateTime = LocalDateTime.now(),
skipTryCompletePickOrderLine: Boolean = false,
deferPersistenceFlush: Boolean = false
): StockOutLine {
require(deltaQty >= BigDecimal.ZERO) { "deltaQty cannot be negative" }

val sol = stockOutLineRepository.findById(stockOutLineId).orElseThrow {
IllegalArgumentException("StockOutLine not found: $stockOutLineId")
}
val sol = stockOutLine

// 1) update stock_out_line qty/status/time
val currentQty = BigDecimal(sol.qty?.toString() ?: "0")
@@ -1923,7 +1946,9 @@ fun applyStockOutLineDelta(
if (!operator.isNullOrBlank()) {
sol.modifiedBy = operator
}
val savedSol = stockOutLineRepository.saveAndFlush(sol)
val savedSol =
if (deferPersistenceFlush) stockOutLineRepository.save(sol)
else stockOutLineRepository.saveAndFlush(sol)

// Nothing to post if no delta
if (deltaQty == BigDecimal.ZERO || !isPickEnd) {
@@ -1955,7 +1980,8 @@ fun applyStockOutLineDelta(
if (!operator.isNullOrBlank()) {
latestLotLine.modifiedBy = operator
}
inventoryLotLineRepository.saveAndFlush(latestLotLine)
if (deferPersistenceFlush) inventoryLotLineRepository.save(latestLotLine)
else inventoryLotLineRepository.saveAndFlush(latestLotLine)
}
} else {
val lotUpdateResult = inventoryLotLineService.updateInventoryLotLineQuantities(
@@ -2017,11 +2043,12 @@ fun applyStockOutLineDelta(
this.modifiedBy = operator
}
}
stockLedgerRepository.saveAndFlush(ledger)
if (deferPersistenceFlush) stockLedgerRepository.save(ledger)
else stockLedgerRepository.saveAndFlush(ledger)
}

// 4) existing side-effects keep same behavior
if (isEndStatus(savedSol.status)) {
// 4) existing side-effects keep same behavior (batch submit defers to end of newBatchSubmit)
if (!skipTryCompletePickOrderLine && isEndStatus(savedSol.status)) {
savedSol.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) }
}



+ 21
- 27
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Просмотреть файл

@@ -2317,28 +2317,27 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch(



fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes<StockTransactionResponse> {
open fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes<StockTransactionResponse> {
val startTime = System.currentTimeMillis()
// 添加调试日志
println("Search request received: itemCode=${request.itemCode}, itemName=${request.itemName}, type=${request.type}, startDate=${request.startDate}, endDate=${request.endDate}")
// 验证:itemCode 或 itemName 至少一个不为 null 或空字符串

println(
"Search request received: itemCode=${request.itemCode}, itemName=${request.itemName}, " +
"type=${request.type}, startDate=${request.startDate}, endDate=${request.endDate}"
)

val itemCode = request.itemCode?.trim()?.takeIf { it.isNotEmpty() }
val itemName = request.itemName?.trim()?.takeIf { it.isNotEmpty() }
if (itemCode == null && itemName == null) {
println("Search validation failed: both itemCode and itemName are null/empty")
return RecordsRes(emptyList(), 0)
}
// request.startDate 和 request.endDate 已经是 LocalDate? 类型,不需要转换

val startDate = request.startDate
val endDate = request.endDate
println("Processed params: itemCode=$itemCode, itemName=$itemName, startDate=$startDate, endDate=$endDate")
// 使用 Repository 查询(更简单、更快)

val total = stockLedgerRepository.countStockTransactions(
itemCode = itemCode,
itemName = itemName,
@@ -2346,20 +2345,17 @@ fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes<
startDate = startDate,
endDate = endDate
)
println("Total count: $total")
// 如果 pageSize 是默认值(100)或未设置,使用 total 作为 pageSize

val actualPageSize = if (request.pageSize == 100) {
total.toInt().coerceAtLeast(1)
} else {
request.pageSize
}
// 计算 offset

val offset = request.pageNum * actualPageSize
// 查询所有符合条件的记录

val ledgers = stockLedgerRepository.findStockTransactions(
itemCode = itemCode,
itemName = itemName,
@@ -2367,13 +2363,13 @@ fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes<
startDate = startDate,
endDate = endDate
)
println("Found ${ledgers.size} ledgers")
val transactions = ledgers.map { ledger ->
val stockInLine = ledger.stockInLine
val stockOutLine = ledger.stockOutLine
StockTransactionResponse(
id = stockInLine?.id ?: stockOutLine?.id ?: 0L,
transactionType = if (ledger.inQty != null && ledger.inQty!! > 0) "IN" else "OUT",
@@ -2400,20 +2396,18 @@ fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes<
remarks = stockInLine?.remarks
)
}
// 按 date 排序(从旧到新),如果 date 为 null 则使用 transactionDate 的日期部分

val sortedTransactions = transactions.sortedWith(
compareBy<StockTransactionResponse>(
{ it.date ?: it.transactionDate?.toLocalDate() },
{ it.transactionDate }
)
)
// 应用分页

val paginatedTransactions = sortedTransactions.drop(offset).take(actualPageSize)
val totalTime = System.currentTimeMillis() - startTime
println("Total time (Repository query): ${totalTime}ms, count: ${paginatedTransactions.size}, total: $total")
return RecordsRes(paginatedTransactions, total.toInt())
}
}

+ 3
- 1
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt Просмотреть файл

@@ -61,7 +61,9 @@ data class UpdateStockOutLineStatusRequest(
val qty: Double? = null,
val remarks: String? = null,
val skipLedgerWrite: Boolean? = false,
val skipInventoryWrite: Boolean? = false
val skipInventoryWrite: Boolean? = false,
/** When true (batch submit path): skip per-line POL/PO rollup, conso completion scan, duplicate BagLotLine; caller runs these once at end. */
val deferAggregatePickOrderEffects: Boolean? = false
)
data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest(
val pickOrderLineId: Long,


Загрузка…
Отмена
Сохранить