Bläddra i källkod

update

master
CANCERYS\kw093 4 dagar sedan
förälder
incheckning
9760717ed6
4 ändrade filer med 175 tillägg och 133 borttagningar
  1. +41
    -132
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  2. +99
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  3. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt
  4. +33
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt

+ 41
- 132
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Visa fil

@@ -92,10 +92,18 @@ open class PickExecutionIssueService(
println(" issueCategory: ${request.issueCategory}")
println("========================================")
// 1. 检查是否已经存在相同的 pick execution issue 记录
// 1. 解析 lot:
// request.lotId 在前端目前传的是 inventory_lot_line.id(用于 SOL 关联/计算 bookQty 等)
// 但 pick_execution_issue.lot_id 在 DB 上外键指向 inventory_lot.id
val inventoryLotLine = request.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
val inventoryLotIdForIssue = inventoryLotLine?.inventoryLot?.id

// 2. 检查是否已经存在相同的 pick execution issue 记录(以 inventory_lot.id 去重)
val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
request.pickOrderLineId,
inventoryLotIdForIssue ?: 0L
)
println("Checking for existing issues...")
@@ -119,12 +127,8 @@ open class PickExecutionIssueService(
val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null)
println("Pick order: id=${pickOrder?.id}, code=${pickOrder?.code}, type=${pickOrder?.type?.value}")
// 2. 获取 inventory_lot_line 并计算账面数量 (bookQty)
val inventoryLotLine = request.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}")
// 3. 计算账面数量 (bookQty)(用 inventory_lot_line 快照)
println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}, inventoryLotId=${inventoryLotIdForIssue}")
// 计算账面数量(创建 issue 时的快照)
val bookQty = if (inventoryLotLine != null) {
@@ -138,19 +142,19 @@ open class PickExecutionIssueService(
BigDecimal.ZERO
}
// 3. 获取数量值
// 4. 获取数量值
val requiredQty = request.requiredQty ?: BigDecimal.ZERO
val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO
val missQty = request.missQty ?: BigDecimal.ZERO
val badItemQty = request.badItemQty ?: BigDecimal.ZERO
val badReason = request.badReason ?: "quantity_problem"
val stockOutLines = stockOutLineRepository
val relatedStockOutLines = stockOutLineRepository
.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)

val currentStatus = stockOutLines.firstOrNull()?.status ?: ""
val currentStatus = relatedStockOutLines.firstOrNull()?.status ?: ""

if (currentStatus.equals("pending", ignoreCase = true)
&& actualPickQty > BigDecimal.ZERO
@@ -187,7 +191,7 @@ open class PickExecutionIssueService(
println(" Bad Reason: $badReason")
println(" Book Qty: $bookQty")
// 4. 计算 issueQty(实际的问题数量)
// 5. 计算 issueQty(实际的问题数量)
val issueQty = when {
// Bad item 或 bad package:一律用用户输入的 bad 数量,不用 bookQty - actualPickQty
badItemQty > BigDecimal.ZERO -> {
@@ -215,9 +219,10 @@ open class PickExecutionIssueService(
println("================================================")
println("=== Processing Logic Selection ===")

// 5. 创建 pick execution issue 记录
// 6. 创建 pick execution issue 记录
val issueNo = generateIssueNo()
println("Generated issue number: $issueNo")
val lotNoForIssue = request.lotNo ?: inventoryLotLine?.inventoryLot?.lotNo
val pickExecutionIssue = PickExecutionIssue(
id = null,
@@ -235,8 +240,8 @@ open class PickExecutionIssueService(
itemId = request.itemId,
itemCode = request.itemCode,
itemDescription = request.itemDescription,
lotId = request.lotId,
lotNo = request.lotNo,
lotId = inventoryLotIdForIssue,
lotNo = lotNoForIssue,
storeLocation = request.storeLocation,
requiredQty = request.requiredQty,
actualPickQty = request.actualPickQty,
@@ -265,7 +270,7 @@ open class PickExecutionIssueService(
println(" Handle Status: ${savedIssue.handleStatus}")
println(" Issue Qty: ${savedIssue.issueQty}")
// 6. NEW: Update inventory_lot_line.issueQty
// 7. NEW: Update inventory_lot_line.issueQty(仍然用 lotLineId)
if (request.lotId != null && inventoryLotLine != null) {
println("Updating inventory_lot_line.issueQty...")
// ✅ 修改:如果只有 missQty,不更新 issueQty
@@ -305,100 +310,19 @@ open class PickExecutionIssueService(
}
}
// 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("=== Processing Logic Selection ===")
println("Actual Pick Qty: $actualPickQtyForProcessing")
println("Miss Qty: $missQtyForProcessing")
println("Bad Item Qty: $badItemQtyForProcessing")
println("Bad Reason: ${request.badReason}")
println("Lot ID: $lotId")
println("Item ID: $itemId")
println("================================================")
// 8. 新的统一处理逻辑(根据 badReason 决定处理方式)
when {
// 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0)
actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: Miss Item Only")
handleMissItemOnly(request, missQtyForProcessing)
}
// 情况2: 只有 bad item (badItemQty > 0, missQty = 0)
badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: Bad Item Only")
// NEW: Check bad reason
if (request.badReason == "package_problem") {
println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem")
handleBadItemPackageProblem(request, badItemQtyForProcessing)
} else {
println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly")
// quantity_problem or default: handle as normal bad item
handleBadItemOnly(request, badItemQtyForProcessing)
}
}
// 情况3: 既有 miss item 又有 bad item
missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Both Miss and Bad Item")
// NEW: Check bad reason
if (request.badReason == "package_problem") {
println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem")
handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing)
} else {
println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem")
handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing)
}
}
// 情况4: 有 miss item 的情况(无论 actualPickQty 是多少)
missQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Miss Item With Partial Pick")
handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing)
}
actualPickQtyForProcessing == BigDecimal.ZERO &&
missQtyForProcessing == BigDecimal.ZERO &&
badItemQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: All zero, mark stock out line as completed")
handleAllZeroMarkCompleted(request)
}
// 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item)
actualPickQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Normal Pick")
handleNormalPick(request, actualPickQtyForProcessing)
}
else -> {
println("⚠️ Unknown case: actualPickQty=$actualPickQtyForProcessing, missQty=$missQtyForProcessing, badItemQty=$badItemQtyForProcessing")
}
}
val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null)
val consoCode = pickOrderForCompletion?.consoCode
if (!consoCode.isNullOrBlank()) {
// 优先走原来的 consoCode 逻辑(兼容已有 DO 流程)
println("🔍 Checking if pick order $consoCode should be completed after lot rejection...")
try {
checkAndCompletePickOrder(consoCode)
} catch (e: Exception) {
println("⚠️ Error checking pick order completion by consoCode: ${e.message}")
}
} else if (pickOrderForCompletion != null) {
// 🔁 没有 consoCode 的情况:改用 pickOrderId 去检查是否可以完结
println("🔍 Checking if pick order ${pickOrderForCompletion.code} (ID=${pickOrderForCompletion.id}) " +
"should be completed after lot rejection (no consoCode)...")
try {
checkAndCompletePickOrderByPickOrderId(pickOrderForCompletion.id!!)
} catch (e: Exception) {
println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}")
}
// 7. 按规则:issue form 只记录问题 +(可选)把 SOL 标记为 checked
val stockOutLines = stockOutLineRepository
.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
stockOutLines.forEach { sol ->
sol.status = "checked"
sol.modified = LocalDateTime.now()
sol.modifiedBy = "system"
stockOutLineRepository.save(sol)
}
stockOutLineRepository.flush()
println("=== recordPickExecutionIssue: SUCCESS ===")
println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}")
@@ -433,11 +357,12 @@ open class PickExecutionIssueService(
)
stockOutLines.forEach { sol ->
sol.status = "completed"
// issue form 不完结,只标记 checked,让 submit/batch submit 决定 completed(允许 0)
sol.status = "checked"
sol.modified = LocalDateTime.now()
sol.modifiedBy = "system"
stockOutLineRepository.save(sol)
println("All-zero case: mark stock out line ${sol.id} as completed (qty kept as ${sol.qty})")
println("All-zero case: mark stock out line ${sol.id} as checked (qty kept as ${sol.qty})")
}
stockOutLineRepository.flush()
}
@@ -773,34 +698,18 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac
// ✅ 修改:不更新 unavailableQty(因为不 reject lot)
// ✅ 修改:不 reject stock_out_line,根据 actualPickQty 设置状态
// ✅ 按规则:issue form 不负责完结/数量提交,只记录问题 +(可选)把 SOL 标记为 checked
val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
stockOutLines.forEach { stockOutLine ->
val requiredQty = request.requiredQty?.toDouble() ?: 0.0
val actualPickQtyDouble = actualPickQty.toDouble()
// 设置状态:如果 actualPickQty >= requiredQty,则为 completed,否则为 partially_completed
val newStatus = if (actualPickQtyDouble >= requiredQty) {
"completed"
} else {
"partially_completed"
}
stockOutLine.status = newStatus
stockOutLine.qty = actualPickQtyDouble
stockOutLine.status = "checked"
stockOutLine.modified = LocalDateTime.now()
stockOutLine.modifiedBy = "system"
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
println("Updated stock out line ${stockOutLine.id} status to: ${newStatus} (NOT rejected)")
// ✅ 修复:使用更新前的 onHandQty 计算 balance
val balance = onHandQtyBeforeUpdate - actualPickQtyDouble
createStockLedgerForStockOut(savedStockOutLine, "Nor", balance)
stockOutLineRepository.saveAndFlush(stockOutLine)
println("Issue form: marked stock out line ${stockOutLine.id} as checked (no completion)")
}
// ✅ 修复:检查 pick order line 是否应该标记为完成


+ 99
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Visa fil

@@ -24,6 +24,7 @@ import com.ffii.fpsms.modules.pickOrder.web.models.*
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.StockOut
import com.ffii.fpsms.modules.stock.entity.StockOutLine
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.entity.StockOutRepository
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
@@ -1459,7 +1460,7 @@ open class PickOrderService(
println("=== DEBUG: checkAndCompletePickOrderByConsoCode ===")
println("consoCode: $consoCode")

val stockOut = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null)
val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(consoCode)
if (stockOut == null) {
println("❌ No stock_out found for consoCode: $consoCode")
return MessageResponse(
@@ -3955,6 +3956,103 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto
}
val oldIll = spl.suggestedLotLine

// Load stock out line (if provided) to decide "bind vs split"
val existingSol = if (req.stockOutLineId != null && req.stockOutLineId > 0) {
stockOutLIneRepository.findById(req.stockOutLineId).orElse(null)
} else null
val pickedQty = existingSol?.qty?.let { numToBigDecimal(it as? Number) } ?: zero

// Switch lot rule:
// - actual pick == 0: replace/bind (no new line)
// - actual pick > 0: split remaining qty into a NEW suggested pick lot + stock out line
if (pickedQty.compareTo(zero) > 0) {
val remaining = qtyToHold.subtract(pickedQty)
if (remaining.compareTo(zero) <= 0) {
return MessageResponse(
id = null,
name = "No remaining qty",
code = "REJECT",
type = "pickorder",
message = "Reject switch lot: picked=$pickedQty already >= required=$qtyToHold",
errorPosition = null
)
}

// Move HOLD for remaining qty (old -> new)
if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) {
val oldHold = oldIll.holdQty ?: zero
val newOldHold = oldHold.subtract(remaining)
oldIll.holdQty = if (newOldHold.compareTo(zero) < 0) zero else newOldHold
inventoryLotLineRepository.save(oldIll)

val newHold = (newIll.holdQty ?: zero).add(remaining)
newIll.holdQty = newHold
inventoryLotLineRepository.save(newIll)
}
if (oldIll == null) {
val newHold = (newIll.holdQty ?: zero).add(remaining)
newIll.holdQty = newHold
inventoryLotLineRepository.save(newIll)
}

// Lock current suggestion qty to the picked qty (picked part stays on oldIll)
spl.qty = pickedQty
suggestPickLotRepository.saveAndFlush(spl)

// Create a NEW stock out line + suggestion for the remaining qty
val stockOut: StockOut = if (existingSol != null) {
existingSol.stockOut ?: throw IllegalStateException("Existing StockOutLine has null stockOut")
} else {
val consoCode = pol.pickOrder?.consoCode ?: ""
val existing = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null)
if (existing != null) {
existing
} else {
val handlerId = pol.pickOrder?.assignTo?.id ?: SecurityUtils.getUser().orElse(null)?.id
require(handlerId != null) { "Cannot create StockOut: handlerId is null" }
val newStockOut = StockOut().apply {
this.consoPickOrderCode = consoCode
this.type = pol.pickOrder?.type?.value ?: ""
this.status = StockOutStatus.PENDING.status
this.handler = handlerId
}
stockOutRepository.save(newStockOut)
}
}

val newSol = StockOutLine().apply {
this.stockOut = stockOut
this.pickOrderLine = pol
this.item = pol.item
this.inventoryLotLine = newIll
this.qty = 0.0
this.status = StockOutLineStatus.CHECKED.status
this.startTime = LocalDateTime.now()
this.type = existingSol?.type ?: "Nor"
}
stockOutLIneRepository.saveAndFlush(newSol)

val newSpl = SuggestedPickLot().apply {
this.type = spl.type
this.pickOrderLine = pol
this.suggestedLotLine = newIll
this.stockOutLine = newSol
this.qty = remaining
this.pickSuggested = spl.pickSuggested
}
suggestPickLotRepository.saveAndFlush(newSpl)

val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo
return MessageResponse(
id = null,
name = "Lot substitution confirmed (split line)",
code = "SUCCESS",
type = "pickorder",
message = "Picked=$pickedQty, created new line for remaining=$remaining on lotNo '$newLotNo'",
errorPosition = null
)
}
// If oldIll exists and different: move hold old -> new
if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) {


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt Visa fil

@@ -7,5 +7,7 @@ import java.util.Optional
@Repository
interface StockOutRepository: AbstractRepository<StockOut, Long> {
fun findByConsoPickOrderCode(consoPickOrderCode: String) : Optional<StockOut>
// consoPickOrderCode 可能在 DB 中存在重复,避免 single-result exception
fun findFirstByConsoPickOrderCodeOrderByIdDesc(consoPickOrderCode: String): StockOut?
fun findByStockTakeIdAndDeletedFalse(stockTakeId: Long): StockOut?
}

+ 33
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Visa fil

@@ -72,6 +72,35 @@ private val inventoryLotLineService: InventoryLotLineService,
private val inventoryRepository: InventoryRepository,
private val pickExecutionIssueRepository: PickExecutionIssueRepository
): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) {
private fun isEndStatus(status: String?): Boolean {
val s = status?.trim()?.lowercase() ?: return false
return s == "completed" || s == "rejected" || s == "partially_completed"
}

@Transactional
private fun tryCompletePickOrderLine(pickOrderLineId: Long) {
val sols = stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
if (sols.isEmpty()) return

val allEnded = sols.all { isEndStatus(it.status) }
if (!allEnded) return

val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return
if (pol.status != PickOrderLineStatus.COMPLETED) {
pol.status = PickOrderLineStatus.COMPLETED
pickOrderLineRepository.save(pol)
}

// Optionally bubble up to pick order completion (safe no-op if not ready)
val consoCode = pol.pickOrder?.consoCode
if (!consoCode.isNullOrBlank()) {
try {
pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode)
} catch (e: Exception) {
println("⚠️ Error checking pick order completion for consoCode=$consoCode: ${e.message}")
}
}
}
@Throws(IOException::class)
@Transactional
open fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> {
@@ -624,6 +653,10 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
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


Laddar…
Avbryt
Spara