CANCERYS\kw093 2 dni temu
rodzic
commit
2fe4480a11
5 zmienionych plików z 236 dodań i 306 usunięć
  1. +72
    -254
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  2. +156
    -51
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  3. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  4. +2
    -1
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt
  5. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt

+ 72
- 254
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Wyświetl plik

@@ -12,7 +12,6 @@ import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.service.StockOutLineService
import org.springframework.context.annotation.Lazy

import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService
@@ -32,6 +31,7 @@ import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository
import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum
import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus
import com.ffii.fpsms.modules.stock.web.model.StockOutRequest
import com.ffii.fpsms.modules.stock.web.model.StockOutStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
@@ -59,7 +59,7 @@ open class PickExecutionIssueService(
private val pickOrderRepository: PickOrderRepository,
private val jdbcDao: JdbcDao,
private val stockOutRepository: StockOutRepository,
@Lazy private val stockOutLineService: StockOutLineService,
private val stockOutLineService: StockOutLineService,
private val pickOrderLineRepository: PickOrderLineRepository,
private val doPickOrderService: DoPickOrderService,
private val joPickOrderRepository: JoPickOrderRepository,
@@ -1673,15 +1673,6 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse {
}
val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
val stockOut = createIssueStockOutHeader("MISS_ITEM", issue.issueRemark, handler)
val pickOrderLine = issue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null)
}
val lotLine = issue.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
val item = itemsRepository.findById(issue.itemId).orElse(null)
?: return MessageResponse(
@@ -1693,50 +1684,18 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse {
errorPosition = null
)
// ✅ 修复:在创建和保存 stockOutLine 之前获取 inventory 的当前 onHandQty
// 必须在任何可能触发数据库触发器更新 inventory 的操作之前获取
val inventoryBeforeUpdate = inventoryRepository.findByItemId(issue.itemId).orElse(null)
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
println("=== submitMissItem: Before update ===")
println("Item ID: ${issue.itemId}")
println("Issue Qty: ${issueQty}")
println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}")
println("=====================================")
// 修改:使用 issueQty 创建 stock_out_line
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = issueQty.toDouble() // 使用 issueQty 而不是 missQty
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = "Miss"
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// 修改:使用 issueQty 更新 inventory_lot_line
// 注意:这会触发数据库触发器更新 inventory,所以必须在获取 onHandQtyBeforeUpdate 之后调用
if (issue.lotId != null) {
updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = true) // 使用 issueQty
}
val lotLineId = issue.lotId
?: throw IllegalArgumentException("Issue ${issue.id} has no lot line id")
val savedStockOutLine = stockOutLineService.createStockOut(
StockOutRequest(
inventoryLotLineId = lotLineId,
qty = issueQty.toDouble(),
type = "Miss"
)
)
markIssueHandled(issue, handler)
// ✅ 修复:使用更新前的 onHandQty 计算 balance
// balance = 更新前的 onHandQty - 本次出库数量
val balance = onHandQtyBeforeUpdate - issueQty.toDouble()
println("=== submitMissItem: Creating stock ledger ===")
println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}")
println("Issue Qty (OutQty): ${issueQty.toDouble()}")
println("Calculated Balance: ${balance}")
println("=============================================")
createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Miss")
return MessageResponse(
id = stockOut.id,
id = savedStockOutLine.stockOut?.id,
name = "Success",
code = "SUCCESS",
type = "stock_issue",
@@ -1781,15 +1740,6 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse {
}
val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
val stockOut = createIssueStockOutHeader("BAD_ITEM", issue.issueRemark, handler)
val pickOrderLine = issue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null)
}
val lotLine = issue.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
val item = itemsRepository.findById(issue.itemId).orElse(null)
?: return MessageResponse(
@@ -1801,27 +1751,15 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse {
errorPosition = null
)
// ✅ 修复:在创建和保存 stockOutLine 之前获取 inventory 的当前 onHandQty
val inventoryBeforeUpdate = inventoryRepository.findByItemId(issue.itemId).orElse(null)
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
println("=== submitBadItem: Before update ===")
println("Item ID: ${issue.itemId}")
println("Issue Qty: ${issueQty}")
println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}")
println("====================================")
// 修改:使用 issueQty 创建 stock_out_line
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = issueQty.toDouble() // 使用 issueQty 而不是 badItemQty
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = "Bad"
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
val lotLineId = issue.lotId
?: throw IllegalArgumentException("Issue ${issue.id} has no lot line id")
val savedStockOutLine = stockOutLineService.createStockOut(
StockOutRequest(
inventoryLotLineId = lotLineId,
qty = issueQty.toDouble(),
type = "Bad"
)
)
// ✅ 修复:Bad item 的 issueQty 应该重置为 0,而不是累加/减去
// 先重置 issueQty 为 0
@@ -1834,25 +1772,9 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse {
}
}
// 修改:使用 issueQty 更新 inventory_lot_line(更新 outQty)
if (issue.lotId != null) {
updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = false) // 使用 issueQty
}
markIssueHandled(issue, handler)
// ✅ 修复:使用更新前的 onHandQty 计算 balance
val balance = onHandQtyBeforeUpdate - issueQty.toDouble()
println("=== submitBadItem: Creating stock ledger ===")
println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}")
println("Issue Qty (OutQty): ${issueQty.toDouble()}")
println("Calculated Balance: ${balance}")
println("===========================================")
createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Bad")
return MessageResponse(
id = stockOut.id,
id = savedStockOutLine.stockOut?.id,
name = "Success",
code = "SUCCESS",
type = "stock_issue",
@@ -1874,6 +1796,8 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse {
@Transactional(rollbackFor = [Exception::class])
open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse {
try {
println("=== submitExpiryItem START ===")
println("Request lotLineId=${request.lotLineId}, handler=${request.handler}")
val lotLine = inventoryLotLineRepository.findById(request.lotLineId).orElse(null)
?: return MessageResponse(
id = null,
@@ -1901,6 +1825,7 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse {
val inQty = lotLine.inQty ?: BigDecimal.ZERO
val outQty = lotLine.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.subtract(outQty)
println("LotLine=${lotLine.id}, itemId=${lot?.item?.id}, inQty=$inQty, outQty=$outQty, remainingQty=$remainingQty")
if (remainingQty <= BigDecimal.ZERO) {
return MessageResponse(
@@ -1914,7 +1839,6 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse {
}
val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
val stockOut = createIssueStockOutHeader("EXPIRY_ITEM", "Expired item removal", handler)
val item = lot.item
?: return MessageResponse(
@@ -1926,23 +1850,16 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse {
errorPosition = null
)
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = remainingQty.toDouble()
this.status = StockOutLineStatus.COMPLETE.status
this.type = "Expiry"
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty, isMissItem = false)
// Create stock ledger entry
createStockLedgerForStockOut(savedStockOutLine)
val savedStockOutLine = stockOutLineService.createStockOut(
StockOutRequest(
inventoryLotLineId = lotLine.id!!,
qty = remainingQty.toDouble(),
type = "Expiry"
)
)
println("submitExpiryItem createStockOut SUCCESS: stockOutLineId=${savedStockOutLine.id}, qty=$remainingQty")
return MessageResponse(
id = stockOut.id,
id = savedStockOutLine.stockOut?.id,
name = "Success",
code = "SUCCESS",
type = "stock_issue",
@@ -1950,6 +1867,8 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse {
errorPosition = null
)
} catch (e: Exception) {
println("submitExpiryItem ERROR: ${e.message}")
e.printStackTrace()
return MessageResponse(
id = null,
name = "Error",
@@ -1980,46 +1899,20 @@ open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse
}
val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
val stockOut = createIssueStockOutHeader("MISS_ITEM", "Batch miss item submission", handler)
issues.forEach { issue ->
val issueQty = issue.issueQty ?: BigDecimal.ZERO
val pickOrderLine = issue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null)
}
val lotLine = issue.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
val item = itemsRepository.findById(issue.itemId).orElse(null)
// 修改:使用 issueQty
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = issueQty.toDouble() // 使用 issueQty
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = "Miss"
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// ✅ 修复:在更新 lot line 之前获取 inventory 的当前 onHandQty,用于计算 balance
val inventoryBeforeUpdate = item?.let { inventoryRepository.findByItemId(it.id!!).orElse(null) }
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
// 修改:使用 issueQty
if (issue.lotId != null) {
updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = true) // 使用 issueQty
}
val lotLineId = issue.lotId
?: throw IllegalArgumentException("Issue ${issue.id} has no lot line id")
stockOutLineService.createStockOut(
StockOutRequest(
inventoryLotLineId = lotLineId,
qty = issueQty.toDouble(),
type = "Miss"
)
)
markIssueHandled(issue, handler)
// ✅ 修复:使用更新前的 onHandQty 计算 balance,避免触发器更新后的错误计算
val balance = onHandQtyBeforeUpdate - issueQty.toDouble()
createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Miss")
}
return MessageResponse(
@@ -2061,37 +1954,19 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse {
}
val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
val stockOut = createIssueStockOutHeader("BAD_ITEM", "Batch bad item submission", handler)
issues.forEach { issue ->
val issueQty = issue.issueQty ?: BigDecimal.ZERO
val pickOrderLine = issue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null)
}
val lotLine = issue.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
val item = itemsRepository.findById(issue.itemId).orElse(null)
// 修改:使用 issueQty
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = issueQty.toDouble() // 使用 issueQty
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = "Bad"
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// ✅ 修复:在更新 lot line 之前获取 inventory 的当前 onHandQty,用于计算 balance
val inventoryBeforeUpdate = item?.let { inventoryRepository.findByItemId(it.id!!).orElse(null) }
val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
val lotLineId = issue.lotId
?: throw IllegalArgumentException("Issue ${issue.id} has no lot line id")
stockOutLineService.createStockOut(
StockOutRequest(
inventoryLotLineId = lotLineId,
qty = issueQty.toDouble(),
type = "Bad"
)
)
// ✅ 修复:Bad item 的 issueQty 应该重置为 0,而不是累加/减去
// 先重置 issueQty 为 0
if (issue.lotId != null) {
@@ -2103,15 +1978,7 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse {
}
}
// 修改:使用 issueQty
if (issue.lotId != null) {
updateLotLineAfterIssue(issue.lotId, issueQty, isMissItem = false) // 使用 issueQty
}
markIssueHandled(issue, handler)
// ✅ 修复:使用更新前的 onHandQty 计算 balance,避免触发器更新后的错误计算
val balance = onHandQtyBeforeUpdate - issueQty.toDouble()
createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, "Bad")
}
return MessageResponse(
@@ -2158,32 +2025,16 @@ open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageRespon
val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
// Create single StockOut header for all submissions
val stockOut = createIssueStockOutHeader("EXPIRY_ITEM", "Batch expiry item removal", handler)
lotLines.forEach { lotLine ->
val remainingQty = (lotLine.inQty ?: BigDecimal.ZERO).subtract(lotLine.outQty ?: BigDecimal.ZERO)
// Get item from inventoryLot
val item = lotLine.inventoryLot?.item
// Create StockOutLine
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = remainingQty.toDouble()
this.status = StockOutLineStatus.COMPLETE.status
this.type = "Expiry"
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// Create stock ledger entry
// Update InventoryLotLine
updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty, isMissItem = false)
createStockLedgerForStockOut(savedStockOutLine)
stockOutLineService.createStockOut(
StockOutRequest(
inventoryLotLineId = lotLine.id!!,
qty = remainingQty.toDouble(),
type = "Expiry"
)
)
}
return MessageResponse(
@@ -2746,20 +2597,8 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}")
println("==========================================")
val stockOut = createIssueStockOutHeader(
if (isMissItem) "MISS_ITEM" else "BAD_ITEM",
firstIssue.issueRemark,
handler
)
println("Created stock out header: id=${stockOut.id}, type=${if (isMissItem) "MISS_ITEM" else "BAD_ITEM"}")
val pickOrderLine = firstIssue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null)
}
println("Pick order line: id=${pickOrderLine?.id}")
val lotLine = request.lotId.let {
inventoryLotLineRepository.findById(it).orElse(null)
val lotLine = request.lotId.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}")
@@ -2774,18 +2613,14 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
)
println("Item: id=${item.id}, code=${item.code}")
// Create stock_out_line with custom quantity
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = submitQty.toDouble() // Use custom quantity
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = if (isMissItem) "Miss" else "Bad"
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}")
val savedStockOutLine = stockOutLineService.createStockOut(
StockOutRequest(
inventoryLotLineId = request.lotId,
qty = submitQty.toDouble(),
type = if (isMissItem) "Miss" else "Bad"
)
)
println("Created stock out line via createStockOut: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}")
if (!isMissItem && request.lotId != null) {
val rejectedLines = stockOutLineRepository
@@ -2804,32 +2639,15 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
}
}
// Update inventory_lot_line with custom quantity - pass isMissItem flag
if (request.lotId != null) {
println("Updating lot line after issue: lotId=${request.lotId}, submitQty=$submitQty, isMissItem=$isMissItem")
updateLotLineAfterIssue(request.lotId, submitQty, isMissItem)
}
// Mark all issues as handled
issues.forEach { issue ->
println(" Marking issue ${issue.id} as handled by handler $handler")
markIssueHandled(issue, handler)
}
// ✅ 修复:使用更新前的 onHandQty 计算 balance
val balance = onHandQtyBeforeUpdate - submitQty.toDouble()
println("=== submitIssueWithQty: Creating stock ledger ===")
println("OnHandQty Before Update: ${onHandQtyBeforeUpdate}")
println("Submit Qty (OutQty): ${submitQty.toDouble()}")
println("Calculated Balance: ${balance}")
println("===================================================")
createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad")
println("=== submitIssueWithQty: SUCCESS ===")
return MessageResponse(
id = stockOut.id,
id = savedStockOutLine.stockOut?.id,
name = "Success",
code = "SUCCESS",
type = "stock_issue",


+ 156
- 51
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Wyświetl plik

@@ -626,7 +626,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
}
open fun updateStatus(request: UpdateStockOutLineStatusRequest): MessageResponse {
try {
// 1. 查当前 stockOutLine
// 1. 查当前 stockOutLine(用于日志)
val stockOutLine = stockOutLineRepository.findById(request.id).orElseThrow {
IllegalArgumentException("StockOutLine not found with ID: ${request.id}")
}
@@ -634,54 +634,18 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
println("Updating StockOutLine ID: ${request.id}")
println("Current status: ${stockOutLine.status}")
println("New status: ${request.status}")
if (request.status == "checked") {
stockOutLine.startTime = LocalDateTime.now()
}
if (request.status == "completed") {
// ✅ 修复:如果 startTime 为 null,也设置它(处理直接完成的情况)
if (stockOutLine.startTime == null) {
stockOutLine.startTime = LocalDateTime.now()
}
stockOutLine.endTime = LocalDateTime.now()
}
// 2. 更新自身 status/qty
stockOutLine.status = request.status
if (request.qty != null) {
val currentQty = stockOutLine.qty?.toDouble() ?: 0.0
val newQty = currentQty + request.qty
stockOutLine.qty = (newQty)
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)

val statusLower = request.status.trim().lowercase()
val isPickEnd = statusLower == "completed" || statusLower == "partially_completed"
val deltaQty = request.qty
val skipLedgerWrite = request.skipLedgerWrite == true

// 手工拣货增量扣减时补写出库 ledger(DO/JO 也需要写)
// 批量提交流程会显式传 skipLedgerWrite=true,避免重复写入。
if (!skipLedgerWrite && isPickEnd && deltaQty != null && deltaQty > 0 && savedStockOutLine.id != null) {
val itemId = savedStockOutLine.item?.id

if (itemId != null) {
val invBefore = inventoryRepository.findByItemId(itemId).orElse(null)
val onHandBefore = invBefore?.onHandQty?.toDouble() ?: 0.0

createStockLedgerForPickDelta(
stockOutLineId = savedStockOutLine.id!!,
deltaQty = BigDecimal(deltaQty.toString()),
onHandQtyBeforeUpdate = onHandBefore
)
}
}
val savedStockOutLine = applyStockOutLineDelta(
stockOutLineId = request.id,
deltaQty = BigDecimal((request.qty ?: 0.0).toString()),
newStatus = request.status,
skipInventoryWrite = request.skipInventoryWrite == true,
skipLedgerWrite = request.skipLedgerWrite == true
)
println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}")
// If this stock out line is in end status, try completing its pick order line
if (isEndStatus(savedStockOutLine.status)) {
savedStockOutLine.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) }
}
try {
val item = savedStockOutLine.item
val inventoryLotLine = savedStockOutLine.inventoryLotLine
val reqDeltaQty = request.qty ?: 0.0
// 只在状态为 completed 或 partially_completed,且数量增加时创建 BagLotLine
val isCompletedOrPartiallyCompleted = request.status == "completed" ||
@@ -691,8 +655,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
if (item?.isBag == true &&
inventoryLotLine != null &&
isCompletedOrPartiallyCompleted &&
request.qty != null &&
request.qty > 0) {
reqDeltaQty > 0) {
println(" Item isBag=true, creating BagLotLine...")
@@ -706,7 +669,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
lotId = inventoryLotLine.inventoryLot?.id ?: 0L,
itemId = item.id!!,
lotNo = lotNo,
stockQty = request.qty.toInt(),
stockQty = reqDeltaQty.toInt(),
date = LocalDate.now(),
time = LocalTime.now(),
stockOutLineId = savedStockOutLine.id
@@ -733,7 +696,7 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
}
// 4. 自动刷 pickOrderLine 状态
val pickOrderLine = stockOutLine.pickOrderLine
val pickOrderLine = savedStockOutLine.pickOrderLine
if (pickOrderLine != null) {
checkIsStockOutLineCompleted(pickOrderLine.id)
// 5. 自动刷 pickOrder 状态
@@ -1263,6 +1226,11 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
// 修复:从数据库获取当前实际数量
val stockOutLine = stockOutLines[line.stockOutLineId]
?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found")
val currentStatus = stockOutLine.status?.trim()?.lowercase()
if (currentStatus == "completed" || currentStatus == "complete") {
println(" Skipping already completed stockOutLineId=${line.stockOutLineId}")
return@forEach
}
val currentActual = (stockOutLine.qty ?: 0.0).toBigDecimal()
val targetActual = line.actualPickQty ?: BigDecimal.ZERO
@@ -1287,7 +1255,8 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
id = line.stockOutLineId,
status = newStatus, // 例如前端传来的 "completed"
qty = 0.0, // 不改变现有 qty
skipLedgerWrite = true
skipLedgerWrite = true,
skipInventoryWrite = true
)
)
@@ -1301,7 +1270,8 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
id = line.stockOutLineId,
status = newStatus,
qty = submitQty.toDouble(),
skipLedgerWrite = true
skipLedgerWrite = true,
skipInventoryWrite = true
)
)

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

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

// 1) update stock_out_line qty/status/time
val currentQty = BigDecimal(sol.qty?.toString() ?: "0")
val newQty = currentQty + deltaQty
sol.qty = newQty.toDouble()

if (!newStatus.isNullOrBlank()) {
sol.status = newStatus
}
val statusLower = sol.status?.trim()?.lowercase() ?: ""
val isPickEnd = statusLower == "completed" || statusLower == "partially_completed"

if (statusLower == "checked" && sol.startTime == null) {
sol.startTime = eventTime
}
if (statusLower == "completed") {
if (sol.startTime == null) sol.startTime = eventTime
sol.endTime = eventTime
}

if (!operator.isNullOrBlank()) {
sol.modifiedBy = operator
}
val savedSol = stockOutLineRepository.saveAndFlush(sol)

// Nothing to post if no delta
if (deltaQty == BigDecimal.ZERO || !isPickEnd) {
return savedSol
}

val postingType = (typeOverride ?: savedSol.type ?: "").trim().lowercase()
val isIssuePosting = postingType == "miss" || postingType == "bad" || postingType == "expiry"

// 2) inventory_lot_line + inventory
if (!skipInventoryWrite) {
val lotLine = savedSol.inventoryLotLine
if (lotLine != null) {
if (isIssuePosting) {
val latestLotLine = inventoryLotLineRepository.findById(lotLine.id!!).orElse(null)
if (latestLotLine != null) {
val currentHoldQty = latestLotLine.holdQty ?: BigDecimal.ZERO
val currentOutQty = latestLotLine.outQty ?: BigDecimal.ZERO
latestLotLine.holdQty = currentHoldQty.subtract(deltaQty).coerceAtLeast(BigDecimal.ZERO)
latestLotLine.outQty = currentOutQty.add(deltaQty)
latestLotLine.modified = eventTime
if (!operator.isNullOrBlank()) {
latestLotLine.modifiedBy = operator
}
inventoryLotLineRepository.saveAndFlush(latestLotLine)
}
} else {
val lotUpdateResult = inventoryLotLineService.updateInventoryLotLineQuantities(
UpdateInventoryLotLineQuantitiesRequest(
inventoryLotLineId = lotLine.id!!,
qty = deltaQty,
operation = "pick"
)
)
if (lotUpdateResult.code != "SUCCESS") {
throw IllegalStateException(
"Failed to update inventory lot line ${lotLine.id} with pick operation: ${lotUpdateResult.message}"
)
}
}
}

val itemId = savedSol.item?.id
if (itemId != null) {
val inv = inventoryRepository.findByItemId(itemId).orElse(null)
if (inv != null) {
val zero = BigDecimal.ZERO
inv.onHandQty = (inv.onHandQty ?: zero).minus(deltaQty)
if (!isIssuePosting) {
inv.onHoldQty = (inv.onHoldQty ?: zero).minus(deltaQty)
}
inventoryRepository.save(inv)
}
}
}

// 3) stock_ledger (shared source with createStockOut/createStockLedgerForStockOut style)
if (!skipLedgerWrite) {
val item = savedSol.item ?: return savedSol
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return savedSol

// unified balance source: latest ledger first, fallback inventory.onHand
val latestLedger = stockLedgerRepository.findLatestByItemId(item.id!!).firstOrNull()
val previousBalance = latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val outQty = deltaQty.toDouble()
val newBalance = previousBalance - outQty

val ledger = StockLedger().apply {
this.stockOutLine = savedSol
this.inventory = inventory
this.inQty = null
this.outQty = outQty
this.balance = newBalance
this.type = typeOverride ?: savedSol.type ?: "NOR"
this.itemId = item.id
this.itemCode = item.code
this.uomId =
itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
?: inventory.uom?.id
this.date = eventTime.toLocalDate()
if (!operator.isNullOrBlank()) {
this.createdBy = operator
this.modifiedBy = operator
}
}
stockLedgerRepository.saveAndFlush(ledger)
}

// 4) existing side-effects keep same behavior
if (isEndStatus(savedSol.status)) {
savedSol.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) }
}

return savedSol
}
}

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Wyświetl plik

@@ -363,6 +363,9 @@ class StockTakeRecordService(
approverBadQty = stockTakeRecord?.approverBadQty,
finalQty = stockTakeLine?.finalQty,
bookQty = stockTakeRecord?.bookQty,
stockTakeSection = stockTakeRecord?.stockTakeSection ?: warehouse?.stockTakeSection,
stockTakeSectionDescription = warehouse?.stockTakeSectionDescription,
stockTakerName = stockTakeRecord?.stockTakerName,
)
}


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt Wyświetl plik

@@ -60,7 +60,8 @@ data class UpdateStockOutLineStatusRequest(
val status: String,
val qty: Double? = null,
val remarks: String? = null,
val skipLedgerWrite: Boolean? = false
val skipLedgerWrite: Boolean? = false,
val skipInventoryWrite: Boolean? = false
)
data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest(
val pickOrderLineId: Long,


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt Wyświetl plik

@@ -61,6 +61,9 @@ data class InventoryLotDetailResponse(
val approverBadQty: BigDecimal? = null,
val finalQty: BigDecimal? = null,
val bookQty: BigDecimal? = null,
val stockTakeSection: String? = null,
val stockTakeSectionDescription: String? = null,
val stockTakerName: String? = null,
)
data class InventoryLotLineListRequest(
val warehouseCode: String


Ładowanie…
Anuluj
Zapisz