Ver código fonte

stock issue

master
CANCERYS\kw093 2 semanas atrás
pai
commit
31c94a9fe8
9 arquivos alterados com 513 adições e 240 exclusões
  1. +0
    -3
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  2. +1
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/models/QcCategoryWithQcItemCount.kt
  3. +6
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt
  4. +461
    -197
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  5. +14
    -14
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt
  6. +16
    -25
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt
  7. +1
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  8. +6
    -0
      src/main/resources/db/changelog/changes/20260118_01_Enson/01_alter_table.sql
  9. +8
    -0
      src/main/resources/db/changelog/changes/20260118_01_Enson/02_alter_table.sql

+ 0
- 3
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Ver arquivo

@@ -1,3 +0,0 @@




+ 1
- 0
src/main/java/com/ffii/fpsms/modules/master/web/models/QcCategoryWithQcItemCount.kt Ver arquivo

@@ -9,3 +9,4 @@ data class QcCategoryWithQcItemCount(
)




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

@@ -67,6 +67,10 @@ val doPickOrderId: Long? = null,

@Column(name = "bad_item_qty", precision = 10, scale = 2)
val badItemQty: BigDecimal = BigDecimal.ZERO,
@Column(name = "book_qty", precision = 10, scale = 2)
val bookQty: BigDecimal = BigDecimal.ZERO,
@Column(name = "issue_qty", precision = 10, scale = 2)
val issueQty: BigDecimal = BigDecimal.ZERO,

@Column(name = "issue_remark", columnDefinition = "TEXT")
val issueRemark: String? = null,
@@ -120,6 +124,8 @@ val doPickOrderId: Long? = null,
storeLocation = null,
requiredQty = null,
actualPickQty = null,
bookQty = BigDecimal.ZERO,
issueQty = BigDecimal.ZERO,
missQty = BigDecimal.ZERO,
badItemQty = BigDecimal.ZERO,
issueRemark = null,


+ 461
- 197
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Ver arquivo

@@ -11,7 +11,7 @@ import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
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.web.model.PickExecutionIssueRequest
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import org.springframework.stereotype.Service
@@ -38,7 +38,10 @@ import com.ffii.fpsms.modules.stock.entity.StockOut
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.common.CodeGenerator
import com.ffii.fpsms.modules.pickOrder.web.models.SubmitIssueRequest

import com.ffii.fpsms.modules.stock.entity.StockLedger
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository

@Service
open class PickExecutionIssueService(
private val pickExecutionIssueRepository: PickExecutionIssueRepository,
@@ -53,151 +56,223 @@ open class PickExecutionIssueService(
private val doPickOrderService: DoPickOrderService,
private val joPickOrderRepository: JoPickOrderRepository,
private val joPickOrderRecordRepository: JoPickOrderRecordRepository,
private val itemsRepository: ItemsRepository
private val itemsRepository: ItemsRepository,
private val stockLedgerRepository: StockLedgerRepository

) {

@Transactional(rollbackFor = [Exception::class])
open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse {
try {
// 1. 检查是否已经存在相同的 pick execution issue 记录
val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse {
try {
// 1. 检查是否已经存在相同的 pick execution issue 记录
val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
if (existingIssues.isNotEmpty()) {
return MessageResponse(
id = null,
name = "Pick execution issue already exists",
code = "DUPLICATE",
type = "pick_execution_issue",
message = "A pick execution issue for this lot has already been recorded",
errorPosition = null
)
}
val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null)
// 2. 获取 inventory_lot_line 并计算账面数量 (bookQty)
val inventoryLotLine = request.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
// 计算账面数量(创建 issue 时的快照)
val bookQty = if (inventoryLotLine != null) {
val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO
val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
inQty.subtract(outQty) // bookQty = inQty - outQty
} else {
BigDecimal.ZERO
}
// 3. 获取数量值
val requiredQty = request.requiredQty ?: BigDecimal.ZERO
val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO
val missQty = request.missQty ?: BigDecimal.ZERO
val badItemQty = request.badItemQty ?: BigDecimal.ZERO
// 4. 验证逻辑:如果 actualPickQty == requiredQty,missQty 必须为 0
if (actualPickQty == requiredQty && missQty > BigDecimal.ZERO) {
return MessageResponse(
id = null,
name = "Invalid issue",
code = "INVALID",
type = "pick_execution_issue",
message = "If actual pick qty equals required qty, miss qty must be 0",
errorPosition = null
)
}
// 5. 计算 issueQty(实际的问题数量)
val issueQty = when {
// 情况1: 已拣完但有坏品
actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> {
badItemQty // issueQty = badItemQty
}
if (existingIssues.isNotEmpty()) {
return MessageResponse(
id = null,
name = "Pick execution issue already exists",
code = "DUPLICATE",
type = "pick_execution_issue",
message = "A pick execution issue for this lot has already been recorded",
errorPosition = null
)
// 情况2: 未拣完(有缺失或坏品)
actualPickQty < requiredQty -> {
// issueQty = bookQty - actualPickQty
// 这是实际缺失的数量(账面应该有的数量 - 实际拣到的数量)
val calculatedIssueQty = bookQty.subtract(actualPickQty)
// 验证:如果用户报告了 missQty,它应该 <= issueQty(但允许用户报告的值)
if (missQty > BigDecimal.ZERO && missQty > calculatedIssueQty) {
println("⚠️ Warning: User reported missQty (${missQty}) exceeds calculated issueQty (${calculatedIssueQty})")
println(" BookQty: ${bookQty}, ActualPickQty: ${actualPickQty}")
}
calculatedIssueQty
}
val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null)
// 2. 创建 pick execution issue 记录
val pickExecutionIssue = PickExecutionIssue(
id = null, // 添加 id
pickOrderId = request.pickOrderId,
pickOrderCode = request.pickOrderCode,
pickOrderCreateDate = request.pickOrderCreateDate,
pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(),
pickOrderLineId = request.pickOrderLineId,
issueNo = generateIssueNo(),
joPickOrderId = pickOrder?.jobOrder?.id, // 添加
doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null, // 添加
issueCategory = IssueCategory.valueOf(
request.issueCategory ?: "lot_issue"
),
itemId = request.itemId,
itemCode = request.itemCode,
itemDescription = request.itemDescription,
lotId = request.lotId,
lotNo = request.lotNo,
storeLocation = request.storeLocation,
requiredQty = request.requiredQty,
actualPickQty = request.actualPickQty,
missQty = request.missQty,
badItemQty = request.badItemQty,
issueRemark = request.issueRemark,
pickerName = request.pickerName, // 确保从 request 中获取
handleStatus = HandleStatus.pending, // 添加
handleDate = null, // 添加
handledBy = request.handledBy,
created = LocalDateTime.now(),
createdBy = "system",
version = 0, // 添加
modified = LocalDateTime.now(),
modifiedBy = "system",
deleted = false // 添加
)
else -> BigDecimal.ZERO
}
println("=== PICK EXECUTION ISSUE PROCESSING ===")
println("Required Qty: ${requiredQty}")
println("Actual Pick Qty: ${actualPickQty}")
println("Miss Qty (Reported): ${missQty}")
println("Bad Item Qty: ${badItemQty}")
println("Book Qty (inQty - outQty): ${bookQty}")
println("Issue Qty (Calculated): ${issueQty}")
println("Lot ID: ${request.lotId}")
println("Item ID: ${request.itemId}")
println("================================================")
// 6. 创建 pick execution issue 记录
val pickExecutionIssue = PickExecutionIssue(
id = null,
pickOrderId = request.pickOrderId,
pickOrderCode = request.pickOrderCode,
pickOrderCreateDate = request.pickOrderCreateDate,
pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(),
pickOrderLineId = request.pickOrderLineId,
issueNo = generateIssueNo(),
joPickOrderId = pickOrder?.jobOrder?.id,
doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null,
issueCategory = IssueCategory.valueOf(
request.issueCategory ?: "lot_issue"
),
itemId = request.itemId,
itemCode = request.itemCode,
itemDescription = request.itemDescription,
lotId = request.lotId,
lotNo = request.lotNo,
storeLocation = request.storeLocation,
requiredQty = request.requiredQty,
actualPickQty = request.actualPickQty,
missQty = request.missQty,
badItemQty = request.badItemQty,
bookQty = bookQty, // 添加账面数量
issueQty = issueQty, // 添加计算的问题数量
issueRemark = request.issueRemark,
pickerName = request.pickerName,
handleStatus = HandleStatus.pending,
handleDate = null,
handledBy = request.handledBy,
created = LocalDateTime.now(),
createdBy = "system",
version = 0,
modified = LocalDateTime.now(),
modifiedBy = "system",
deleted = false
)

val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue)
val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue)

// 3. 获取相关数据
val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO
val missQty = request.missQty ?: BigDecimal.ZERO
val badItemQty = request.badItemQty ?: BigDecimal.ZERO
val lotId = request.lotId
val itemId = request.itemId
// 7. 获取相关数据用于后续处理
val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO
val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO
val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO
val lotId = request.lotId
val itemId = request.itemId

println("=== PICK EXECUTION ISSUE PROCESSING (NEW LOGIC) ===")
println("Actual Pick Qty: ${actualPickQty}")
println("Miss Qty: ${missQty}")
println("Bad Item Qty: ${badItemQty}")
println("Lot ID: ${lotId}")
println("Item ID: ${itemId}")
println("================================================")
println("=== PICK EXECUTION ISSUE PROCESSING (NEW LOGIC) ===")
println("Actual Pick Qty: ${actualPickQtyForProcessing}")
println("Miss Qty: ${missQtyForProcessing}")
println("Bad Item Qty: ${badItemQtyForProcessing}")
println("Lot ID: ${lotId}")
println("Item ID: ${itemId}")
println("================================================")

// 4. 新的统一处理逻辑
when {
// 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0)
actualPickQty == BigDecimal.ZERO && missQty > BigDecimal.ZERO && badItemQty == BigDecimal.ZERO -> {
handleMissItemOnly(request, missQty)
}
// 情况2: 只有 bad item (badItemQty > 0, missQty = 0)
badItemQty > BigDecimal.ZERO && missQty == BigDecimal.ZERO -> {
handleBadItemOnly(request, badItemQty)
}
// 情况3: 既有 miss item 又有 bad item
missQty > BigDecimal.ZERO && badItemQty > BigDecimal.ZERO -> {
handleBothMissAndBadItem(request, missQty, badItemQty)
}
// 修复:情况4: 有 miss item 的情况(无论 actualPickQty 是多少)
missQty > BigDecimal.ZERO -> {
handleMissItemWithPartialPick(request, actualPickQty, missQty)
}
// 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item)
actualPickQty > BigDecimal.ZERO -> {
handleNormalPick(request, actualPickQty)
}
else -> {
println("Unknown case: actualPickQty=${actualPickQty}, missQty=${missQty}, badItemQty=${badItemQty}")
}
// 8. 新的统一处理逻辑
when {
// 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0)
actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> {
handleMissItemOnly(request, missQtyForProcessing)
}

val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null)
val consoCode = pickOrderForCompletion?.consoCode
if (consoCode != null) {
println("🔍 Checking if pick order $consoCode should be completed after lot rejection...")
try {
checkAndCompletePickOrder(consoCode)
} catch (e: Exception) {
println("⚠️ Error checking pick order completion: ${e.message}")
}
// 情况2: 只有 bad item (badItemQty > 0, missQty = 0)
badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> {
handleBadItemOnly(request, badItemQtyForProcessing)
}
return MessageResponse(
id = savedIssue.id,
name = "Pick execution issue recorded successfully",
code = "SUCCESS",
type = "pick_execution_issue",
message = "Pick execution issue recorded successfully",
errorPosition = null
)
// 情况3: 既有 miss item 又有 bad item
missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> {
handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing)
}
// 修复:情况4: 有 miss item 的情况(无论 actualPickQty 是多少)
missQtyForProcessing > BigDecimal.ZERO -> {
handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing)
}
// 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item)
actualPickQtyForProcessing > BigDecimal.ZERO -> {
handleNormalPick(request, actualPickQtyForProcessing)
}
else -> {
println("Unknown case: actualPickQty=${actualPickQtyForProcessing}, missQty=${missQtyForProcessing}, badItemQty=${badItemQtyForProcessing}")
}
}

} catch (e: Exception) {
println("=== ERROR IN recordPickExecutionIssue ===")
e.printStackTrace()
return MessageResponse(
id = null,
name = "Failed to record pick execution issue",
code = "ERROR",
type = "pick_execution_issue",
message = "Error: ${e.message}",
errorPosition = null
)
val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null)
val consoCode = pickOrderForCompletion?.consoCode
if (consoCode != null) {
println("🔍 Checking if pick order $consoCode should be completed after lot rejection...")
try {
checkAndCompletePickOrder(consoCode)
} catch (e: Exception) {
println("⚠️ Error checking pick order completion: ${e.message}")
}
}

return MessageResponse(
id = savedIssue.id,
name = "Pick execution issue recorded successfully",
code = "SUCCESS",
type = "pick_execution_issue",
message = "Pick execution issue recorded successfully",
errorPosition = null
)

} catch (e: Exception) {
println("=== ERROR IN recordPickExecutionIssue ===")
e.printStackTrace()
return MessageResponse(
id = null,
name = "Failed to record pick execution issue",
code = "ERROR",
type = "pick_execution_issue",
message = "Error: ${e.message}",
errorPosition = null
)
}
}
private fun generateIssueNo(): String {
val now = LocalDateTime.now()
val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM"))
@@ -383,7 +458,7 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.save(inventoryLotLine)
inventoryLotLineRepository.saveAndFlush(inventoryLotLine) // 使用 saveAndFlush
println("Miss item with partial pick: Updated lot ${lotId}")
println(" - Added to outQty: ${actualPickQty} (${currentOutQty} -> ${newOutQty})")
@@ -395,7 +470,33 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac
updateInventoryUnavailableQty(itemId, missQty)
// 修复5:更新 stock_out_line 状态为 rejected(因为还有 miss item)
updateStockOutLineStatus(request, "rejected")
// 修复:同时创建 stock_ledger 记录
val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
stockOutLines.forEach { stockOutLine ->
stockOutLine.status = "rejected"
// 更新 qty 为 actualPickQty
if (request.actualPickQty != null) {
stockOutLine.qty = request.actualPickQty.toDouble()
println("Updated stock out line ${stockOutLine.id} qty to: ${request.actualPickQty}")
}
stockOutLine.modified = LocalDateTime.now()
stockOutLine.modifiedBy = "system"
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
println("Updated stock out line ${stockOutLine.id} status to: rejected")
// 修复:为实际拣货的部分创建 stock_ledger 记录(即使状态是 rejected)
// 因为这部分确实从库存中出库了
if (request.actualPickQty != null && request.actualPickQty > BigDecimal.ZERO) {
createStockLedgerForStockOut(savedStockOutLine, "Nor") // 实际拣货的部分
}
}
// 重新建议拣货批次(针对 miss 的数量)
try {
@@ -417,12 +518,12 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
if (inventoryLotLine != null) {
// 修复:Miss item 意味着剩余的所有物品都找不到
val currentInQty = inventoryLotLine.inQty ?: BigDecimal.ZERO
val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
val remainingQty = currentInQty.minus(currentOutQty)
// 标记批次为 unavailable
inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
@@ -434,16 +535,15 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD
println(" - Unavailable qty (should be remaining qty): ${remainingQty}")
}
// 修复:更新 inventory 表的 unavailableQty
// 应该是剩余的数量,而不是 missQty
val currentInQty = inventoryLotLine?.inQty ?: BigDecimal.ZERO
val currentOutQty = inventoryLotLine?.outQty ?: BigDecimal.ZERO
val remainingQty = currentInQty.minus(currentOutQty)
// 修复:只增加剩余数量,不要重复计算 missQty
updateInventoryUnavailableQty(itemId, remainingQty)
// 修复:更新 stock_out_line 状态为 rejected
updateStockOutLineStatus(request, "rejected")
// 重新建议拣货批次
@@ -555,11 +655,14 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty
stockOutLine.qty = actualPickQtyDouble // 直接设置,不累积
stockOutLine.modified = LocalDateTime.now()
stockOutLine.modifiedBy = "system"
stockOutLineRepository.save(stockOutLine)
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) // 使用 saveAndFlush
println("Updated stock out line ${stockOutLine.id}: status=${newStatus}, qty=${actualPickQtyDouble}")
// 修复:为正常拣货创建 stock_ledger 记录
createStockLedgerForStockOut(savedStockOutLine, "Nor")
}

// 修复:更新 inventory_lot_line 的 outQty
val lotId = request.lotId ?: return
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
@@ -572,7 +675,7 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty
inventoryLotLine.outQty = newOutQty
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.save(inventoryLotLine)
inventoryLotLineRepository.saveAndFlush(inventoryLotLine) // 使用 saveAndFlush 确保触发器执行
println("Updated inventory lot line ${lotId} outQty: ${currentOutQty} -> ${newOutQty}")
}
@@ -801,22 +904,68 @@ open fun createBatchReleaseIssue(
}
}

open fun getMissItemList(issueCategory: String = "lot_issue"): List<PickExecutionIssue> {
open fun getMissItemList(issueCategory: String = "lot_issue"): List<StockIssueResponse> {
val category = try {
IssueCategory.valueOf(issueCategory)
} catch (e: Exception) {
IssueCategory.lot_issue
}
return pickExecutionIssueRepository.findMissItemList(category).filter { it.handleStatus != HandleStatus.completed }
return pickExecutionIssueRepository.findMissItemList(category)
.filter { it.handleStatus != HandleStatus.completed }
.map { issue ->
StockIssueResponse(
id = issue.id ?: 0L,
itemId = issue.itemId,
itemCode = issue.itemCode,
itemDescription = issue.itemDescription,
lotId = issue.lotId,
lotNo = issue.lotNo,
storeLocation = issue.storeLocation,
requiredQty = issue.requiredQty,
actualPickQty = issue.actualPickQty,
missQty = issue.missQty,
badItemQty = issue.badItemQty,
bookQty = issue.bookQty ?: BigDecimal.ZERO,
issueQty = issue.issueQty ?: BigDecimal.ZERO, // 返回计算的问题数量
issueRemark = issue.issueRemark,
pickerName = issue.pickerName,
handleStatus = issue.handleStatus.name,
handleDate = issue.handleDate,
handledBy = issue.handledBy
)
}
}

open fun getBadItemList(issueCategory: String = "lot_issue"): List<PickExecutionIssue> {
open fun getBadItemList(issueCategory: String = "lot_issue"): List<StockIssueResponse> {
val category = try {
IssueCategory.valueOf(issueCategory)
} catch (e: Exception) {
IssueCategory.lot_issue
}
return pickExecutionIssueRepository.findBadItemListByCategory(category).filter { it.handleStatus != HandleStatus.completed }
return pickExecutionIssueRepository.findBadItemListByCategory(category)
.filter { it.handleStatus != HandleStatus.completed }
.map { issue ->
StockIssueResponse(
id = issue.id ?: 0L,
itemId = issue.itemId,
itemCode = issue.itemCode,
itemDescription = issue.itemDescription,
lotId = issue.lotId,
lotNo = issue.lotNo,
storeLocation = issue.storeLocation,
requiredQty = issue.requiredQty,
actualPickQty = issue.actualPickQty,
missQty = issue.missQty,
badItemQty = issue.badItemQty,
bookQty = issue.bookQty ?: BigDecimal.ZERO,
issueQty = issue.issueQty ?: BigDecimal.ZERO, // 返回计算的问题数量
issueRemark = issue.issueRemark,
pickerName = issue.pickerName,
handleStatus = issue.handleStatus.name,
handleDate = issue.handleDate,
handledBy = issue.handledBy
)
}
}
open fun getBadItemOnlyList(): List<PickExecutionIssue> {
return pickExecutionIssueRepository.findBadItemOnlyList(IssueCategory.lot_issue)
@@ -856,13 +1005,15 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse {
errorPosition = null
)
if (issue.missQty <= BigDecimal.ZERO || issue.deleted) {
// 修改:使用 issueQty 而不是 missQty
val issueQty = issue.issueQty ?: BigDecimal.ZERO
if (issueQty <= BigDecimal.ZERO || issue.deleted) {
return MessageResponse(
id = null,
name = "Error",
code = "INVALID",
type = "stock_issue",
message = "Invalid issue or no miss quantity",
message = "Invalid issue or no issue quantity",
errorPosition = null
)
}
@@ -888,22 +1039,26 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse {
errorPosition = null
)
// 修改:使用 issueQty 创建 stock_out_line
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = issue.missQty.toDouble()
this.status = StockOutLineStatus.PENDING.status
this.qty = issueQty.toDouble() // 使用 issueQty 而不是 missQty
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = "Miss"
}
stockOutLineRepository.save(stockOutLine)
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// 修改:使用 issueQty 更新 inventory_lot_line
if (issue.lotId != null) {
updateLotLineAfterIssue(issue.lotId, issue.missQty)
updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty
}
markIssueHandled(issue, handler)
// Create stock ledger entry
createStockLedgerForStockOut(savedStockOutLine)
return MessageResponse(
id = stockOut.id,
name = "Success",
@@ -923,7 +1078,6 @@ open fun submitMissItem(request: SubmitIssueRequest): MessageResponse {
)
}
}

@Transactional(rollbackFor = [Exception::class])
open fun submitBadItem(request: SubmitIssueRequest): MessageResponse {
try {
@@ -937,13 +1091,15 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse {
errorPosition = null
)
if (issue.badItemQty <= BigDecimal.ZERO || issue.deleted) {
// 修改:使用 issueQty 而不是 badItemQty
val issueQty = issue.issueQty ?: BigDecimal.ZERO
if (issueQty <= BigDecimal.ZERO || issue.deleted) {
return MessageResponse(
id = null,
name = "Error",
code = "INVALID",
type = "stock_issue",
message = "Invalid issue or no bad item quantity",
message = "Invalid issue or no issue quantity",
errorPosition = null
)
}
@@ -969,22 +1125,26 @@ open fun submitBadItem(request: SubmitIssueRequest): MessageResponse {
errorPosition = null
)
// 修改:使用 issueQty 创建 stock_out_line
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = issue.badItemQty.toDouble()
this.status = StockOutLineStatus.PENDING.status
this.qty = issueQty.toDouble() // 使用 issueQty 而不是 badItemQty
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = "Bad"
}
stockOutLineRepository.save(stockOutLine)
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// 修改:使用 issueQty 更新 inventory_lot_line
if (issue.lotId != null) {
updateLotLineAfterIssue(issue.lotId, issue.badItemQty)
updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty
}
markIssueHandled(issue, handler)
// Create stock ledger entry
createStockLedgerForStockOut(savedStockOutLine)
return MessageResponse(
id = stockOut.id,
name = "Success",
@@ -1065,12 +1225,16 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse {
this.inventoryLotLine = lotLine
this.item = item
this.qty = remainingQty.toDouble()
this.status = StockOutLineStatus.PENDING.status
this.status = StockOutLineStatus.COMPLETE.status
this.type = "Expiry"
}
stockOutLineRepository.save(stockOutLine)
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty)
updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty)
// Create stock ledger entry
createStockLedgerForStockOut(savedStockOutLine)
return MessageResponse(
id = stockOut.id,
name = "Success",
@@ -1091,12 +1255,12 @@ open fun submitExpiryItem(request: SubmitExpiryRequest): MessageResponse {
}
}

// Fix batchSubmitMissItem method (around line 835):
@Transactional(rollbackFor = [Exception::class])
open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse {
try {
// 修改:检查 issueQty 而不是 missQty
val issues = pickExecutionIssueRepository.findAllById(request.issueIds)
.filter { it.missQty > BigDecimal.ZERO && !it.deleted }
.filter { (it.issueQty ?: BigDecimal.ZERO) > BigDecimal.ZERO && !it.deleted }
if (issues.isEmpty()) {
return MessageResponse(
@@ -1110,44 +1274,40 @@ open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse
}
val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
// Create single StockOut header for all submissions
val stockOut = createIssueStockOutHeader("MISS_ITEM", "Batch miss item submission", handler)
issues.forEach { issue ->
val issueQty = issue.issueQty ?: BigDecimal.ZERO
// Get pickOrderLine if available
val pickOrderLine = issue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null)
}
// Get inventoryLotLine
val lotLine = issue.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
// Get item from issue (it has itemId)
val item = itemsRepository.findById(issue.itemId).orElse(null)

// Create StockOutLine
// 修改:使用 issueQty
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = issue.missQty.toDouble()
this.status = StockOutLineStatus.PENDING.status
this.qty = issueQty.toDouble() // 使用 issueQty
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = "Miss"
}
stockOutLineRepository.save(stockOutLine)
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// Update InventoryLotLine
// 修改:使用 issueQty
if (issue.lotId != null) {
updateLotLineAfterIssue(issue.lotId, issue.missQty)
updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty
}
// Mark issue as handled
markIssueHandled(issue, handler)
createStockLedgerForStockOut(savedStockOutLine)
}
return MessageResponse(
@@ -1170,12 +1330,12 @@ open fun batchSubmitMissItem(request: BatchSubmitIssueRequest): MessageResponse
}
}

// Fix batchSubmitBadItem method (around line 890) - same pattern:
@Transactional(rollbackFor = [Exception::class])
open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse {
try {
// 修改:检查 issueQty 而不是 badItemQty
val issues = pickExecutionIssueRepository.findAllById(request.issueIds)
.filter { it.badItemQty > BigDecimal.ZERO && !it.deleted }
.filter { (it.issueQty ?: BigDecimal.ZERO) > BigDecimal.ZERO && !it.deleted }
if (issues.isEmpty()) {
return MessageResponse(
@@ -1189,44 +1349,40 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse {
}
val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
// Create single StockOut header for all submissions
val stockOut = createIssueStockOutHeader("BAD_ITEM", "Batch bad item submission", handler)
issues.forEach { issue ->
val issueQty = issue.issueQty ?: BigDecimal.ZERO
// Get pickOrderLine if available
val pickOrderLine = issue.pickOrderLineId?.let {
pickOrderLineRepository.findById(it).orElse(null)
}
// Get inventoryLotLine
val lotLine = issue.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
// Get item from issue
val item = itemsRepository.findById(issue.itemId).orElse(null)

// Create StockOutLine
// 修改:使用 issueQty
val stockOutLine = StockOutLine().apply {
this.stockOut = stockOut
this.inventoryLotLine = lotLine
this.item = item
this.qty = issue.badItemQty.toDouble()
this.status = StockOutLineStatus.PENDING.status
this.qty = issueQty.toDouble() // 使用 issueQty
this.status = StockOutLineStatus.COMPLETE.status
this.pickOrderLine = pickOrderLine
this.type = "Bad"
}
stockOutLineRepository.save(stockOutLine)
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// Update InventoryLotLine
// 修改:使用 issueQty
if (issue.lotId != null) {
updateLotLineAfterIssue(issue.lotId, issue.badItemQty)
updateLotLineAfterIssue(issue.lotId, issueQty) // 使用 issueQty
}
// Mark issue as handled
markIssueHandled(issue, handler)
createStockLedgerForStockOut(savedStockOutLine)
}
return MessageResponse(
@@ -1248,7 +1404,6 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse {
)
}
}

// Fix batchSubmitExpiryItem method (around line 945):
@Transactional(rollbackFor = [Exception::class])
open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageResponse {
@@ -1289,12 +1444,17 @@ open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageRespon
this.inventoryLotLine = lotLine
this.item = item
this.qty = remainingQty.toDouble()
this.status = StockOutLineStatus.PENDING.status
this.status = StockOutLineStatus.COMPLETE.status
this.type = "Expiry"
}
stockOutLineRepository.save(stockOutLine)
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
// Create stock ledger entry
// Update InventoryLotLine
updateLotLineAfterIssue(lotLine.id ?: 0L, remainingQty)
createStockLedgerForStockOut(savedStockOutLine)
}
return MessageResponse(
@@ -1360,7 +1520,111 @@ private fun updateLotLineAfterIssue(lotLineId: Long, qty: BigDecimal) {
lotLine.modified = LocalDateTime.now()
lotLine.modifiedBy = "system"
inventoryLotLineRepository.save(lotLine)
// 修复:使用 saveAndFlush 确保立即提交到数据库,触发触发器
inventoryLotLineRepository.saveAndFlush(lotLine)
updateInventoryAfterLotLineChange(lotLine)
}
}
private fun updateInventoryAfterLotLineChange(lotLine: InventoryLotLine) {
try {
val item = lotLine.inventoryLot?.item ?: return
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
// 使用 SQL 查询计算所有相关 lot lines 的总和
val sql = """
SELECT
SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) as totalOnHandQty,
SUM(COALESCE(ill.holdQty, 0)) as totalOnHoldQty,
SUM(CASE
WHEN ill.status = 'unavailable'
THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)
ELSE 0
END) as totalUnavailableQty
FROM inventory_lot_line ill
INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId
WHERE il.itemId = :itemId
AND ill.deleted = 0
""".trimIndent()
val result = jdbcDao.queryForMap(sql, mapOf("itemId" to item.id!!)).orElse(null)
if (result != null) {
val totalOnHandQty = (result["totalOnHandQty"] as? Number)?.let {
BigDecimal(it.toString())
} ?: BigDecimal.ZERO
val totalOnHoldQty = (result["totalOnHoldQty"] as? Number)?.let {
BigDecimal(it.toString())
} ?: BigDecimal.ZERO
val totalUnavailableQty = (result["totalUnavailableQty"] as? Number)?.let {
BigDecimal(it.toString())
} ?: BigDecimal.ZERO
// 更新 inventory
inventory.onHandQty = totalOnHandQty
inventory.onHoldQty = totalOnHoldQty
inventory.unavailableQty = totalUnavailableQty
inventory.status = if (totalOnHandQty.subtract(totalOnHoldQty).subtract(totalUnavailableQty) > BigDecimal.ZERO) {
"available"
} else {
"unavailable"
}
inventory.modified = LocalDateTime.now()
inventory.modifiedBy = "system"
inventoryRepository.saveAndFlush(inventory)
println("=== MANUALLY UPDATED INVENTORY ===")
println("Item ID: ${item.id}")
println("Total OnHandQty: ${totalOnHandQty}")
println("Total OnHoldQty: ${totalOnHoldQty}")
println("Total UnavailableQty: ${totalUnavailableQty}")
println("Status: ${inventory.status}")
println("==================================")
}
} catch (e: Exception) {
println("Error updating inventory manually: ${e.message}")
e.printStackTrace()
}
}
private fun createStockLedgerForStockOut(stockOutLine: StockOutLine, ledgerType: String? = null) {
val item = stockOutLine.item ?: return
val outQty = stockOutLine.qty?.toDouble() ?: 0.0
// 修复:重新查询 inventory 以获取触发器更新后的最新 onHandQty
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
// 计算新的 balance = 当前 onHandQty - 本次出库数量
val currentOnHandQty = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val newBalance = currentOnHandQty - outQty
// 确保 balance 不为负数
val finalBalance = if (newBalance < 0) 0.0 else newBalance
// 使用传入的 ledgerType,如果没有则使用 stockOutLine.type
val ledgerTypeToUse = ledgerType ?: stockOutLine.type ?: "Nor"
val stockLedger = StockLedger().apply {
this.stockOutLine = stockOutLine
this.inventory = inventory
this.inQty = null
this.outQty = outQty
this.balance = finalBalance
this.type = ledgerTypeToUse // 使用指定的 type
this.itemId = item.id
this.itemCode = item.code
this.date = LocalDate.now()
}
stockLedgerRepository.saveAndFlush(stockLedger)
println("=== CREATED STOCK LEDGER ===")
println("Type: ${ledgerTypeToUse}")
println("OutQty: ${outQty}")
println("Balance: ${finalBalance}")
println("===========================")
}
}

+ 14
- 14
src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt Ver arquivo

@@ -5,9 +5,9 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssue
import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum
import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService // 修复导入路径
import com.ffii.fpsms.modules.stock.web.model.PickExecutionIssueRequest
import org.springframework.web.bind.annotation.*
import com.ffii.fpsms.modules.pickOrder.web.models.*
import org.springframework.web.bind.annotation.*


@RestController
@RequestMapping("/pickExecution")
@@ -45,18 +45,18 @@ class PickExecutionIssueController(
return pickExecutionIssueService.getBadItemList(status)
}
@GetMapping("/issues/missItem")
fun getMissItemIssues(
@RequestParam(required = false, defaultValue = "lot_issue") issueCategory: String
): List<PickExecutionIssue> {
return pickExecutionIssueService.getMissItemList(issueCategory)
}
@GetMapping("/issues/badItem")
fun getBadItemIssues(
@RequestParam(required = false, defaultValue = "lot_issue") issueCategory: String
): List<PickExecutionIssue> {
return pickExecutionIssueService.getBadItemList(issueCategory)
}
fun getMissItemIssues(
@RequestParam(required = false, defaultValue = "lot_issue") issueCategory: String
): List<StockIssueResponse> {
return pickExecutionIssueService.getMissItemList(issueCategory)
}
@GetMapping("/issues/badItem")
fun getBadItemIssues(
@RequestParam(required = false, defaultValue = "lot_issue") issueCategory: String
): List<StockIssueResponse> {
return pickExecutionIssueService.getBadItemList(issueCategory)
}

@GetMapping("/issues/expiryItem")
fun getExpiryItemIssues(): List<ExpiryItemResponse> {


+ 16
- 25
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt Ver arquivo

@@ -1,5 +1,5 @@
// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/stock/web/model/PickExecutionIssueRequest.kt
package com.ffii.fpsms.modules.stock.web.model
package com.ffii.fpsms.modules.pickOrder.web.models

import java.math.BigDecimal
import java.time.LocalDate
@@ -30,33 +30,24 @@ data class PickExecutionIssueRequest(
val handledBy: Long? = null
)

data class SubmitIssueRequest(
val issueId: Long,
val handler: Long? = null,
)

data class BatchSubmitIssueRequest(
val issueIds: List<Long>,
val handler: Long? = null,
)

data class SubmitExpiryRequest(
val lotLineId: Long,
val handler: Long? = null,
)

data class BatchSubmitExpiryRequest(
val lotLineIds: List<Long>,
val handler: Long? = null,
)
data class ExpiryItemResponse(
val id: Long, // InventoryLotLine ID
data class StockIssueResponse(
val id: Long,
val itemId: Long,
val itemCode: String,
val itemCode: String?,
val itemDescription: String?,
val lotId: Long, // InventoryLot ID
val lotId: Long?,
val lotNo: String?,
val storeLocation: String?,
val expiryDate: LocalDate?,
val remainingQty: BigDecimal,
val requiredQty: BigDecimal?,
val actualPickQty: BigDecimal?,
val missQty: BigDecimal,
val badItemQty: BigDecimal,
val bookQty: BigDecimal,
val issueQty: BigDecimal,
val issueRemark: String?,
val pickerName: String?,
val handleStatus: String,
val handleDate: LocalDate?,
val handledBy: Long?
)

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Ver arquivo

@@ -844,7 +844,7 @@ println("Keeping all suggestions (including rejected ones for display)")
)
}
println("Creating stock out line for suggestion")
createStockOutLineForSuggestion(suggestion, pickOrderToResuggest)
//createStockOutLineForSuggestion(suggestion, pickOrderToResuggest)
println("Stock out line created")
}
}


+ 6
- 0
src/main/resources/db/changelog/changes/20260118_01_Enson/01_alter_table.sql Ver arquivo

@@ -0,0 +1,6 @@
--liquibase formatted sql
--changeset author:add_time_fields_to_productprocessline

ALTER TABLE `pick_execution_issue`
ADD COLUMN `bookQty` INT(11) NULL AFTER `bad_item_qty`,
ADD COLUMN `issueQty` INT(11) NULL AFTER `bookQty`;

+ 8
- 0
src/main/resources/db/changelog/changes/20260118_01_Enson/02_alter_table.sql Ver arquivo

@@ -0,0 +1,8 @@
--liquibase formatted sql
--changeset author:add_time_fields_to_productprocessline

ALTER TABLE `pick_execution_issue`
DROP COLUMN `bookQty`,
DROP COLUMN `issueQty`,
ADD COLUMN `book_qty` DECIMAL(10,2) NULL AFTER `bad_item_qty`,
ADD COLUMN `issue_qty` DECIMAL(10,2) NULL AFTER `book_qty`;

Carregando…
Cancelar
Salvar