|
|
@@ -46,6 +46,9 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository |
|
|
import java.time.LocalTime |
|
|
import java.time.LocalTime |
|
|
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository |
|
|
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository |
|
|
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository |
|
|
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 |
|
|
import java.util.UUID |
|
|
@Service |
|
|
@Service |
|
|
open class StockOutLineService( |
|
|
open class StockOutLineService( |
|
|
@@ -75,6 +78,10 @@ private val inventoryLotLineService: InventoryLotLineService, |
|
|
private val pickExecutionIssueRepository: PickExecutionIssueRepository, |
|
|
private val pickExecutionIssueRepository: PickExecutionIssueRepository, |
|
|
private val itemUomService: ItemUomService, |
|
|
private val itemUomService: ItemUomService, |
|
|
): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { |
|
|
): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { |
|
|
|
|
|
|
|
|
|
|
|
@PersistenceContext |
|
|
|
|
|
private lateinit var entityManager: EntityManager |
|
|
|
|
|
|
|
|
private fun isEndStatus(status: String?): Boolean { |
|
|
private fun isEndStatus(status: String?): Boolean { |
|
|
val s = status?.trim()?.lowercase() ?: return false |
|
|
val s = status?.trim()?.lowercase() ?: return false |
|
|
return s == "completed" || s == "rejected" || s == "partially_completed" |
|
|
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) |
|
|
@Throws(IOException::class) |
|
|
@Transactional |
|
|
@Transactional |
|
|
open fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> { |
|
|
open fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> { |
|
|
@@ -378,7 +401,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { |
|
|
// }) |
|
|
// }) |
|
|
// } |
|
|
// } |
|
|
@Transactional |
|
|
@Transactional |
|
|
fun checkIsStockOutLineCompleted(pickOrderLineId: Long) { |
|
|
|
|
|
|
|
|
fun checkIsStockOutLineCompleted(pickOrderLineId: Long, quiet: Boolean = false) { |
|
|
val allStockOutLines = stockOutLineRepository |
|
|
val allStockOutLines = stockOutLineRepository |
|
|
.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) |
|
|
.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) |
|
|
|
|
|
|
|
|
@@ -409,7 +432,9 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { |
|
|
acc + (issue.issueQty ?: BigDecimal.ZERO) |
|
|
acc + (issue.issueQty ?: BigDecimal.ZERO) |
|
|
} |
|
|
} |
|
|
} catch (e: Exception) { |
|
|
} 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 |
|
|
BigDecimal.ZERO |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@@ -430,14 +455,16 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { |
|
|
// ✅ 现在的规则:这三类状态都算“已结束” |
|
|
// ✅ 现在的规则:这三类状态都算“已结束” |
|
|
!(isComplete || isRejected || isPartiallyComplete) |
|
|
!(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()) { |
|
|
if (unfinishedLine.isEmpty()) { |
|
|
// set pick order line status to complete |
|
|
// set pick order line status to complete |
|
|
val pol = pickOrderLineRepository.findById(pickOrderLineId).orElseThrow() |
|
|
val pol = pickOrderLineRepository.findById(pickOrderLineId).orElseThrow() |
|
|
@@ -447,11 +474,28 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { |
|
|
this.status = PickOrderLineStatus.COMPLETED |
|
|
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") |
|
|
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) { |
|
|
private fun completeDoIfAllPickOrdersCompleted(pickOrderId: Long) { |
|
|
// 1) 先用 line 关联找 do_pick_order_id |
|
|
// 1) 先用 line 关联找 do_pick_order_id |
|
|
val lines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(pickOrderId) |
|
|
val lines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(pickOrderId) |
|
|
@@ -634,60 +678,66 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { |
|
|
println("Updating StockOutLine ID: ${request.id}") |
|
|
println("Updating StockOutLine ID: ${request.id}") |
|
|
println("Current status: ${stockOutLine.status}") |
|
|
println("Current status: ${stockOutLine.status}") |
|
|
println("New status: ${request.status}") |
|
|
println("New status: ${request.status}") |
|
|
|
|
|
val deferAggregate = request.deferAggregatePickOrderEffects == true |
|
|
val savedStockOutLine = applyStockOutLineDelta( |
|
|
val savedStockOutLine = applyStockOutLineDelta( |
|
|
stockOutLineId = request.id, |
|
|
|
|
|
|
|
|
stockOutLine = stockOutLine, |
|
|
deltaQty = BigDecimal((request.qty ?: 0.0).toString()), |
|
|
deltaQty = BigDecimal((request.qty ?: 0.0).toString()), |
|
|
newStatus = request.status, |
|
|
newStatus = request.status, |
|
|
skipInventoryWrite = request.skipInventoryWrite == true, |
|
|
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}") |
|
|
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 { |
|
|
} 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. 如果被拒绝,触发特殊处理 |
|
|
// 3. 如果被拒绝,触发特殊处理 |
|
|
if (request.status == "rejected" || request.status == "REJECTED") { |
|
|
if (request.status == "rejected" || request.status == "REJECTED") { |
|
|
@@ -695,26 +745,18 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { |
|
|
handleLotRejectionFromStockOutLine(savedStockOutLine) |
|
|
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( |
|
|
return MessageResponse( |
|
|
id = savedStockOutLine.id, |
|
|
id = savedStockOutLine.id, |
|
|
name = savedStockOutLine.inventoryLotLine?.inventoryLot?.lotNo?: "", |
|
|
name = savedStockOutLine.inventoryLotLine?.inventoryLot?.lotNo?: "", |
|
|
@@ -1213,140 +1255,120 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { |
|
|
// 先前預載的 inventories 從未使用(save 已註解),已移除以避免批量提交無故失敗。 |
|
|
// 先前預載的 inventories 從未使用(save 已註解),已移除以避免批量提交無故失敗。 |
|
|
val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId } |
|
|
val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId } |
|
|
println("Loading ${lotLineIds.size} lot lines...") |
|
|
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 { |
|
|
} 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 } |
|
|
val stockOutLineIds = request.lines.map { it.stockOutLineId } |
|
|
println("Loading ${stockOutLineIds.size} stock out lines...") |
|
|
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 -> |
|
|
request.lines.forEach { line: QrPickSubmitLineRequest -> |
|
|
val lineTrace = "$traceId|SOL=${line.stockOutLineId}" |
|
|
val lineTrace = "$traceId|SOL=${line.stockOutLineId}" |
|
|
try { |
|
|
try { |
|
|
println("[$lineTrace] Processing line, noLot=${line.noLot}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val solEntity = stockOutLinesById[line.stockOutLineId] |
|
|
|
|
|
?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found") |
|
|
|
|
|
|
|
|
if (line.noLot) { |
|
|
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 |
|
|
processedIds += line.stockOutLineId |
|
|
println("[$lineTrace] noLot processed (status->completed, qty=0)") |
|
|
|
|
|
return@forEach |
|
|
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") { |
|
|
if (currentStatus == "completed" || currentStatus == "complete") { |
|
|
println("[$lineTrace] Skip because current status is already completed") |
|
|
|
|
|
return@forEach |
|
|
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 { |
|
|
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) { |
|
|
if (item?.isBag == true && inventoryLotLine != null && submitQty > BigDecimal.ZERO) { |
|
|
println(" Item isBag=true, creating BagLotLine...") |
|
|
|
|
|
|
|
|
|
|
|
// 根据 itemId 查找对应的 Bag |
|
|
|
|
|
val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!) |
|
|
val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!) |
|
|
|
|
|
|
|
|
if (bag != null) { |
|
|
if (bag != null) { |
|
|
val lotNo = inventoryLotLine.inventoryLot?.lotNo |
|
|
val lotNo = inventoryLotLine.inventoryLot?.lotNo |
|
|
if (lotNo != null) { |
|
|
if (lotNo != null) { |
|
|
@@ -1354,29 +1376,21 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { |
|
|
bagId = bag.id!!, |
|
|
bagId = bag.id!!, |
|
|
lotId = inventoryLotLine.inventoryLot?.id ?: 0L, |
|
|
lotId = inventoryLotLine.inventoryLot?.id ?: 0L, |
|
|
itemId = item.id!!, |
|
|
itemId = item.id!!, |
|
|
stockOutLineId = stockOutLine.id , |
|
|
|
|
|
|
|
|
stockOutLineId = savedSol.id, |
|
|
lotNo = lotNo, |
|
|
lotNo = lotNo, |
|
|
stockQty = submitQty.toInt(), // 转换为 Int |
|
|
|
|
|
|
|
|
stockQty = submitQty.toInt(), |
|
|
date = LocalDate.now(), |
|
|
date = LocalDate.now(), |
|
|
time = LocalTime.now() |
|
|
time = LocalTime.now() |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
bagService.createBagLotLinesByBagId(createBagLotLineRequest) |
|
|
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) { |
|
|
} catch (e: Exception) { |
|
|
println(" Error creating BagLotLine: ${e.message}") |
|
|
println(" Error creating BagLotLine: ${e.message}") |
|
|
e.printStackTrace() |
|
|
e.printStackTrace() |
|
|
// 不中断主流程,只记录错误 |
|
|
|
|
|
} |
|
|
} |
|
|
processedIds += line.stockOutLineId |
|
|
processedIds += line.stockOutLineId |
|
|
println("[$lineTrace] Line processed successfully") |
|
|
|
|
|
} catch (e: Exception) { |
|
|
} catch (e: Exception) { |
|
|
println("[$lineTrace] Error processing line: ${e.message}") |
|
|
println("[$lineTrace] Error processing line: ${e.message}") |
|
|
e.printStackTrace() |
|
|
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 { |
|
|
try { |
|
|
checkIsStockOutLineCompleted(pickOrderLineId) |
|
|
|
|
|
|
|
|
refreshPickOrderHeaderIfAllLinesCompleted(pickOrderId) |
|
|
} catch (e: Exception) { |
|
|
} 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 ===") |
|
|
println("=== Checking completion by consoCode for ${affectedConsoCodes.size} affected consoCodes after batch submit ===") |
|
|
affectedConsoCodes.forEach { consoCode -> |
|
|
affectedConsoCodes.forEach { consoCode -> |
|
|
@@ -1559,63 +1573,72 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun createStockLedgerForPickDelta( |
|
|
private fun createStockLedgerForPickDelta( |
|
|
stockOutLineId: Long, |
|
|
|
|
|
|
|
|
stockOutLine: StockOutLine, |
|
|
deltaQty: BigDecimal, |
|
|
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) { |
|
|
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 |
|
|
return |
|
|
} |
|
|
} |
|
|
val item = sol.item |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val item = stockOutLine.item |
|
|
if (item == null) { |
|
|
if (item == null) { |
|
|
println("${tracePrefix}Skip ledger creation: stockOutLine.item is null") |
|
|
|
|
|
|
|
|
if (flushAfterSave) { |
|
|
|
|
|
println("${tracePrefix}Skip ledger creation: stockOutLine.item is null") |
|
|
|
|
|
} |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) |
|
|
val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) |
|
|
if (inventory == null) { |
|
|
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 |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val previousBalance = resolvePreviousBalance( |
|
|
val previousBalance = resolvePreviousBalance( |
|
|
itemId = item.id!!, |
|
|
itemId = item.id!!, |
|
|
inventory = inventory, |
|
|
inventory = inventory, |
|
|
onHandQtyBeforeUpdate = onHandQtyBeforeUpdate |
|
|
onHandQtyBeforeUpdate = onHandQtyBeforeUpdate |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val newBalance = previousBalance - deltaQty.toDouble() |
|
|
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 { |
|
|
val ledger = StockLedger().apply { |
|
|
this.stockOutLine = sol |
|
|
|
|
|
|
|
|
this.stockOutLine = stockOutLine |
|
|
this.inventory = inventory |
|
|
this.inventory = inventory |
|
|
this.inQty = null |
|
|
this.inQty = null |
|
|
this.outQty = deltaQty.toDouble() |
|
|
|
|
|
|
|
|
this.outQty = deltaQty.toDouble() |
|
|
this.balance = newBalance |
|
|
this.balance = newBalance |
|
|
this.type = "NOR" |
|
|
|
|
|
|
|
|
this.type = "NOR" |
|
|
this.itemId = item.id |
|
|
this.itemId = item.id |
|
|
this.itemCode = item.code |
|
|
this.itemCode = item.code |
|
|
this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id |
|
|
this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id |
|
|
?: inventory.uom?.id |
|
|
?: inventory.uom?.id |
|
|
this.date = LocalDate.now() |
|
|
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( |
|
|
private fun resolvePreviousBalance( |
|
|
@@ -1886,20 +1909,20 @@ open fun batchScan(request: com.ffii.fpsms.modules.stock.web.model.BatchScanRequ |
|
|
} |
|
|
} |
|
|
@Transactional |
|
|
@Transactional |
|
|
fun applyStockOutLineDelta( |
|
|
fun applyStockOutLineDelta( |
|
|
stockOutLineId: Long, |
|
|
|
|
|
|
|
|
stockOutLine: StockOutLine, |
|
|
deltaQty: BigDecimal, |
|
|
deltaQty: BigDecimal, |
|
|
newStatus: String?, |
|
|
newStatus: String?, |
|
|
typeOverride: String? = null, |
|
|
typeOverride: String? = null, |
|
|
skipInventoryWrite: Boolean = false, |
|
|
skipInventoryWrite: Boolean = false, |
|
|
skipLedgerWrite: Boolean = false, |
|
|
skipLedgerWrite: Boolean = false, |
|
|
operator: String? = null, |
|
|
operator: String? = null, |
|
|
eventTime: LocalDateTime = LocalDateTime.now() |
|
|
|
|
|
|
|
|
eventTime: LocalDateTime = LocalDateTime.now(), |
|
|
|
|
|
skipTryCompletePickOrderLine: Boolean = false, |
|
|
|
|
|
deferPersistenceFlush: Boolean = false |
|
|
): StockOutLine { |
|
|
): StockOutLine { |
|
|
require(deltaQty >= BigDecimal.ZERO) { "deltaQty cannot be negative" } |
|
|
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 |
|
|
// 1) update stock_out_line qty/status/time |
|
|
val currentQty = BigDecimal(sol.qty?.toString() ?: "0") |
|
|
val currentQty = BigDecimal(sol.qty?.toString() ?: "0") |
|
|
@@ -1923,7 +1946,9 @@ fun applyStockOutLineDelta( |
|
|
if (!operator.isNullOrBlank()) { |
|
|
if (!operator.isNullOrBlank()) { |
|
|
sol.modifiedBy = operator |
|
|
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 |
|
|
// Nothing to post if no delta |
|
|
if (deltaQty == BigDecimal.ZERO || !isPickEnd) { |
|
|
if (deltaQty == BigDecimal.ZERO || !isPickEnd) { |
|
|
@@ -1955,7 +1980,8 @@ fun applyStockOutLineDelta( |
|
|
if (!operator.isNullOrBlank()) { |
|
|
if (!operator.isNullOrBlank()) { |
|
|
latestLotLine.modifiedBy = operator |
|
|
latestLotLine.modifiedBy = operator |
|
|
} |
|
|
} |
|
|
inventoryLotLineRepository.saveAndFlush(latestLotLine) |
|
|
|
|
|
|
|
|
if (deferPersistenceFlush) inventoryLotLineRepository.save(latestLotLine) |
|
|
|
|
|
else inventoryLotLineRepository.saveAndFlush(latestLotLine) |
|
|
} |
|
|
} |
|
|
} else { |
|
|
} else { |
|
|
val lotUpdateResult = inventoryLotLineService.updateInventoryLotLineQuantities( |
|
|
val lotUpdateResult = inventoryLotLineService.updateInventoryLotLineQuantities( |
|
|
@@ -2017,11 +2043,12 @@ fun applyStockOutLineDelta( |
|
|
this.modifiedBy = operator |
|
|
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) } |
|
|
savedSol.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|