瀏覽代碼

update

master
CANCERYS\kw093 3 月之前
父節點
當前提交
5af3e1bc62
共有 19 個檔案被更改,包括 2115 行新增187 行删除
  1. +131
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt
  2. +16
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt
  3. +9
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt
  4. +435
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  5. +787
    -111
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  6. +30
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt
  7. +23
    -4
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt
  8. +1
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt
  9. +26
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt
  10. +10
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/FailInventoryLotLineRepository.kt
  11. +47
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/Failinventorylotline.kt
  12. +3
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  13. +8
    -3
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
  14. +17
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  15. +497
    -67
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  16. +14
    -1
      src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt
  17. +12
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/PickAnotherLotRequest.kt
  18. +5
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/UpdateSuggestedLotLineIdRequest.kt
  19. +44
    -0
      src/main/resources/db/changelog/changes/20250902_02_enson/01_create_group_enson.sql

+ 131
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt 查看文件

@@ -0,0 +1,131 @@
// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt
package com.ffii.fpsms.modules.pickOrder.entity

import jakarta.persistence.*
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime

@Entity
@Table(name = "pick_execution_issue")
class PickExecutionIssue(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,

@Column(name = "pick_order_id", nullable = false)
val pickOrderId: Long,

@Column(name = "pick_order_code", length = 50, nullable = false)
val pickOrderCode: String,

@Column(name = "pick_order_create_date")
val pickOrderCreateDate: LocalDate? = null,

@Column(name = "pick_execution_date")
val pickExecutionDate: LocalDate? = null,

@Column(name = "pick_order_line_id", nullable = false)
val pickOrderLineId: Long,

@Column(name = "item_id", nullable = false)
val itemId: Long,

@Column(name = "item_code", length = 50)
val itemCode: String? = null,

@Column(name = "item_description", length = 255)
val itemDescription: String? = null,

@Column(name = "lot_id")
val lotId: Long? = null,

@Column(name = "lot_no", length = 50)
val lotNo: String? = null,

@Column(name = "store_location", length = 100)
val storeLocation: String? = null,

@Column(name = "required_qty", precision = 10, scale = 2)
val requiredQty: BigDecimal? = null,

@Column(name = "actual_pick_qty", precision = 10, scale = 2)
val actualPickQty: BigDecimal? = null,

@Column(name = "miss_qty", precision = 10, scale = 2)
val missQty: BigDecimal = BigDecimal.ZERO,

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

@Column(name = "issue_remark", columnDefinition = "TEXT")
val issueRemark: String? = null,

@Column(name = "picker_name", length = 100)
val pickerName: String? = null,

@Enumerated(EnumType.STRING)
@Column(name = "handle_status")
val handleStatus: HandleStatus = HandleStatus.pending,

@Column(name = "handle_date")
val handleDate: LocalDate? = null,

@Column(name = "handled_by")
val handledBy: Long? = null,

@Column(name = "created", nullable = false)
val created: LocalDateTime = LocalDateTime.now(),

@Column(name = "createdBy", length = 30)
val createdBy: String? = null,

@Version
@Column(name = "version", nullable = false)
val version: Int = 0,

@Column(name = "modified", nullable = false)
val modified: LocalDateTime = LocalDateTime.now(),

@Column(name = "modifiedBy", length = 30)
val modifiedBy: String? = null,

@Column(name = "deleted", nullable = false)
val deleted: Boolean = false
) {
// ✅ 添加默认构造函数
constructor() : this(
pickOrderId = 0L,
pickOrderCode = "",
pickOrderCreateDate = null,
pickExecutionDate = null,
pickOrderLineId = 0L,
itemId = 0L,
itemCode = null,
itemDescription = null,
lotId = null,
lotNo = null,
storeLocation = null,
requiredQty = null,
actualPickQty = null,
missQty = BigDecimal.ZERO,
badItemQty = BigDecimal.ZERO,
issueRemark = null,
pickerName = null,
handleStatus = HandleStatus.pending,
handleDate = null,
handledBy = null,
created = LocalDateTime.now(),
createdBy = null,
version = 0,
modified = LocalDateTime.now(),
modifiedBy = null,
deleted = false
)
}

enum class HandleStatus {
pending, // ✅ Change to lowercase to match database
handled, // ✅ Change to lowercase to match database
resolved // ✅ Change to lowercase to match database
}

+ 16
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt 查看文件

@@ -0,0 +1,16 @@
// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/stock/entity/PickExecutionIssueRepository.kt
package com.ffii.fpsms.modules.pickOrder.entity

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface PickExecutionIssueRepository : JpaRepository<PickExecutionIssue, Long> {
fun findByPickOrderIdAndDeletedFalse(pickOrderId: Long): List<PickExecutionIssue>
fun findByPickOrderLineIdAndDeletedFalse(pickOrderLineId: Long): List<PickExecutionIssue>
fun findByLotIdAndDeletedFalse(lotId: Long): List<PickExecutionIssue>
fun findByPickOrderLineIdAndLotIdAndDeletedFalse(
pickOrderLineId: Long,
lotId: Long
): List<PickExecutionIssue>
}

+ 9
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt 查看文件

@@ -9,6 +9,7 @@ import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository
import com.ffii.fpsms.modules.user.entity.User
import java.io.Serializable
import java.time.LocalDateTime

@@ -63,4 +64,12 @@ interface PickOrderRepository : AbstractRepository<PickOrder, Long> {
fun findAllByConsoCodeAndStatus(consoCode: String, status: PickOrderStatus): List<PickOrder>

fun findAllByIdIn(id: List<Serializable>): List<PickOrder>

@Query("SELECT p FROM PickOrder p WHERE p.assignTo = :assignTo AND p.status IN :statuses AND p.deleted = false")
fun findAllByAssignToAndStatusIn(@Param("assignTo") assignTo: User, @Param("statuses") statuses: List<PickOrderStatus>): List<PickOrder>
@Query("SELECT p FROM PickOrder p WHERE p.status = :status AND p.deleted = false ORDER BY p.targetDate ASC")
fun findAllByStatusAndDeletedFalse(@Param("status") status: PickOrderStatus): List<PickOrder>
@Query("SELECT p FROM PickOrder p WHERE p.assignTo.id = :assignToId AND p.status IN :statuses AND p.deleted = false")
fun findAllByAssignToIdAndStatusIn(@Param("assignToId") assignToId: Long, @Param("statuses") statuses: List<PickOrderStatus>): List<PickOrder>
}

+ 435
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt 查看文件

@@ -0,0 +1,435 @@
// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
package com.ffii.fpsms.modules.pickOrder.service

import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssue
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository
import com.ffii.fpsms.modules.stock.entity.StockOutLine
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
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.annotation.Propagation
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime

@Service
open class PickExecutionIssueService(
private val pickExecutionIssueRepository: PickExecutionIssueRepository,
private val stockOutLineRepository: StockOutLIneRepository,
private val inventoryLotLineRepository: InventoryLotLineRepository,
private val inventoryRepository: InventoryRepository,
private val suggestedPickLotService: SuggestedPickLotService
) {

@Transactional(rollbackFor = [Exception::class])
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
)
}

// 2. 创建 pick execution issue 记录
val pickExecutionIssue = PickExecutionIssue(
pickOrderId = request.pickOrderId,
pickOrderCode = request.pickOrderCode,
pickOrderCreateDate = request.pickOrderCreateDate,
pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(),
pickOrderLineId = request.pickOrderLineId,
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,
handledBy = request.handledBy,
created = LocalDateTime.now(),
createdBy = "system",
modified = LocalDateTime.now(),
modifiedBy = "system"
)

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

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("================================================")

// 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}")
}
}


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
)
}
}
// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
// ✅ 修复:处理有部分拣货但有 miss item 的情况
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class])
private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, actualPickQty: BigDecimal, missQty: BigDecimal) {
println("=== HANDLING MISS ITEM WITH PARTIAL PICK (FIXED LOGIC) ===")
println("Actual Pick Qty: ${actualPickQty}")
println("Miss Qty: ${missQty}")
val lotId = request.lotId ?: return
val itemId = request.itemId ?: return
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
if (inventoryLotLine != null) {
// ✅ 修复1:只处理已拣货的部分:更新 outQty
val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
val newOutQty = currentOutQty.add(actualPickQty)
inventoryLotLine.outQty = newOutQty
// ✅ 修复2:Miss item 不减少 inQty,而是标记为 unavailable
// 因为 miss item 意味着这些物品实际上不存在或找不到
// 所以应该标记整个批次为 unavailable,而不是减少 inQty
// ✅ 修复3:如果 missQty > 0,标记批次为 unavailable
if (missQty > BigDecimal.ZERO) {
inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE
}
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.save(inventoryLotLine)
println("Miss item with partial pick: Updated lot ${lotId}")
println(" - Added to outQty: ${actualPickQty} (${currentOutQty} -> ${newOutQty})")
println(" - Set status to UNAVAILABLE due to missQty: ${missQty}")
}
// ✅ 修复4:更新 inventory 表的 unavailableQty
// 对于 miss item,应该将 missQty 计入 unavailableQty
updateInventoryUnavailableQty(itemId, missQty)
// ✅ 修复5:更新 stock_out_line 状态为 rejected(因为还有 miss item)
updateStockOutLineStatus(request, "rejected")
// 重新建议拣货批次(针对 miss 的数量)
try {
resuggestPickOrder(request.pickOrderId)
println("Resuggested pick order for miss qty: ${missQty}")
} catch (e: Exception) {
println("Error during resuggest in handleMissItemWithPartialPick: ${e.message}")
}
}

// ✅ 修复:Miss item 处理逻辑
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class])
private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigDecimal) {
println("=== HANDLING MISS ITEM ONLY (FIXED LOGIC) ===")
println("Miss Qty: ${missQty}")
val lotId = request.lotId ?: return
val itemId = request.itemId ?: return
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"
inventoryLotLineRepository.save(inventoryLotLine)
println("Miss item only: Set lot ${lotId} status to UNAVAILABLE")
println(" - Remaining qty: ${remainingQty}")
println(" - Miss qty (user input): ${missQty}")
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")
// 重新建议拣货批次
try {
resuggestPickOrder(request.pickOrderId)
println("Resuggested pick order to find alternative lots for missing qty: ${missQty}")
} catch (e: Exception) {
println("Error during resuggest in handleMissItemOnly: ${e.message}")
}
}

// ✅ 修复:Bad item 处理逻辑
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class])
private fun handleBadItemOnly(request: PickExecutionIssueRequest, badItemQty: BigDecimal) {
println("=== HANDLING BAD ITEM ONLY (FIXED LOGIC) ===")
println("Bad Item Qty: ${badItemQty}")
val lotId = request.lotId ?: return
val itemId = request.itemId ?: return
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
if (inventoryLotLine != null) {
// ✅ 修复:Bad item 不减少 inQty,而是标记为 unavailable
// 因为 bad item 意味着这些物品质量有问题,不能使用
inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.save(inventoryLotLine)
println("Bad item only: Set lot ${lotId} status to UNAVAILABLE")
}
// ✅ 修复:更新 inventory 表的 unavailableQty
updateInventoryUnavailableQty(itemId, badItemQty)
// ✅ 修复:更新 stock_out_line 状态为 rejected
updateStockOutLineStatus(request, "rejected")
// 重新建议拣货批次
try {
resuggestPickOrder(request.pickOrderId)
println("Resuggested pick order for bad item qty: ${badItemQty}")
} catch (e: Exception) {
println("Error during resuggest in handleBadItemOnly: ${e.message}")
}
}

// ✅ 修复:Both miss and bad item 处理逻辑
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class])
private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty: BigDecimal, badItemQty: BigDecimal) {
println("=== HANDLING BOTH MISS AND BAD ITEM (FIXED LOGIC) ===")
println("Miss Qty: ${missQty}, Bad Item Qty: ${badItemQty}")
val lotId = request.lotId ?: return
val itemId = request.itemId ?: return
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
val totalUnavailableQty = missQty.add(badItemQty)
if (inventoryLotLine != null) {
// ✅ 修复:Miss + Bad item 不减少 inQty,而是标记为 unavailable
inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.save(inventoryLotLine)
println("Both miss and bad item: Set lot ${lotId} status to UNAVAILABLE")
println(" - Miss Qty: ${missQty}")
println(" - Bad Item Qty: ${badItemQty}")
println(" - Total Unavailable Qty: ${totalUnavailableQty}")
}
// ✅ 修复:更新 inventory 表的 unavailableQty
updateInventoryUnavailableQty(itemId, totalUnavailableQty)
// ✅ 修复:更新 stock_out_line 状态为 rejected
updateStockOutLineStatus(request, "rejected")
// 重新建议拣货批次
try {
resuggestPickOrder(request.pickOrderId)
println("Resuggested pick order for both miss qty: ${missQty} and bad item qty: ${badItemQty}")
} catch (e: Exception) {
println("Error during resuggest in handleBothMissAndBadItem: ${e.message}")
}
}

// ✅ 修复:正常拣货处理逻辑
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class])
private fun handleNormalPick(request: PickExecutionIssueRequest, actualPickQty: BigDecimal) {
println("=== HANDLING NORMAL PICK ===")
// ✅ 修复:更新 stock_out_line,但不要累积 qty
val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
stockOutLines.forEach { stockOutLine ->
// ✅ 修复:直接设置 qty 为 actualPickQty,不要累积
val requiredQty = request.requiredQty?.toDouble() ?: 0.0
val actualPickQtyDouble = actualPickQty.toDouble()
val newStatus = if (actualPickQtyDouble >= requiredQty) {
"completed"
} else {
"partially_completed"
}
stockOutLine.status = newStatus
stockOutLine.qty = actualPickQtyDouble // ✅ 直接设置,不累积
stockOutLine.modified = LocalDateTime.now()
stockOutLine.modifiedBy = "system"
stockOutLineRepository.save(stockOutLine)
println("Updated stock out line ${stockOutLine.id}: status=${newStatus}, qty=${actualPickQtyDouble}")
}

// ✅ 修复:更新 inventory_lot_line 的 outQty
val lotId = request.lotId ?: return
val inventoryLotLine = inventoryLotLineRepository.findById(lotId).orElse(null)
if (inventoryLotLine != null) {
// ✅ 修复:计算新的 outQty,考虑之前的 outQty
val currentOutQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
val previousPickedQty = currentOutQty.minus(actualPickQty) // 计算之前已拣的数量
val newOutQty = previousPickedQty.add(actualPickQty) // 更新为新的总拣货数量
inventoryLotLine.outQty = newOutQty
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.save(inventoryLotLine)
println("Updated inventory lot line ${lotId} outQty: ${currentOutQty} -> ${newOutQty}")
}
}

// ✅ 新方法:统一更新 inventory 表的 unavailableQty
private fun updateInventoryUnavailableQty(itemId: Long, unavailableQty: BigDecimal) {
try {
println("=== INVENTORY UNAVAILABLE QTY UPDATE (TRIGGER HANDLED) ===")
println("Item ID: ${itemId}")
println("Expected unavailableQty to be added by trigger: ${unavailableQty}")
println("Note: Database trigger will handle unavailableQty calculation")
println("=========================================================")
} catch (e: Exception) {
println("Error in updateInventoryUnavailableQty: ${e.message}")
}
}

// ✅ 修复:更新 stock_out_line 状态
private fun updateStockOutLineStatus(request: PickExecutionIssueRequest, status: String) {
val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
stockOutLines.forEach { stockOutLine ->
stockOutLine.status = status
// ✅ FIX: Update qty to actualPickQty before setting status to rejected
if (status == "rejected" && 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"
stockOutLineRepository.save(stockOutLine)
println("Updated stock out line ${stockOutLine.id} status to: ${status}")
}
}
// ✅ 修复:使用 REQUIRES_NEW 传播级别,避免事务冲突
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = [Exception::class])
private fun resuggestPickOrder(pickOrderId: Long?) {
if (pickOrderId != null) {
try {
val resuggestResult = suggestedPickLotService.resuggestPickOrder(pickOrderId)
println("Resuggest result: ${resuggestResult.code} - ${resuggestResult.message}")
if (resuggestResult.code != "SUCCESS") {
println("Warning: Resuggest failed: ${resuggestResult.message}")
}
} catch (e: Exception) {
println("Error during resuggest: ${e.message}")
e.printStackTrace()
// 不重新抛出异常,避免影响主事务
}
}
}

open fun getPickExecutionIssuesByPickOrder(pickOrderId: Long): List<PickExecutionIssue> {
return pickExecutionIssueRepository.findByPickOrderIdAndDeletedFalse(pickOrderId)
}

open fun getPickExecutionIssuesByPickOrderLine(pickOrderLineId: Long): List<PickExecutionIssue> {
return pickExecutionIssueRepository.findByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
}
}

+ 787
- 111
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
文件差異過大導致無法顯示
查看文件


+ 30
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt 查看文件

@@ -0,0 +1,30 @@
// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt
package com.ffii.fpsms.modules.pickOrder.web

import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssue
import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService // ✅ 修复导入路径
import com.ffii.fpsms.modules.stock.web.model.PickExecutionIssueRequest
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/pickExecution")
class PickExecutionIssueController(
private val pickExecutionIssueService: PickExecutionIssueService
) {

@PostMapping("/recordIssue")
fun recordPickExecutionIssue(@RequestBody request: PickExecutionIssueRequest): MessageResponse {
return pickExecutionIssueService.recordPickExecutionIssue(request)
}

@GetMapping("/issues/pickOrder/{pickOrderId}")
fun getPickExecutionIssuesByPickOrder(@PathVariable pickOrderId: Long): List<PickExecutionIssue> {
return pickExecutionIssueService.getPickExecutionIssuesByPickOrder(pickOrderId)
}

@GetMapping("/issues/pickOrderLine/{pickOrderLineId}")
fun getPickExecutionIssuesByPickOrderLine(@PathVariable pickOrderLineId: Long): List<PickExecutionIssue> {
return pickExecutionIssueService.getPickExecutionIssuesByPickOrderLine(pickOrderLineId)
}
}

+ 23
- 4
src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt 查看文件

@@ -32,7 +32,7 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.LocalDate
import com.ffii.fpsms.modules.pickOrder.entity.projection.PickOrderGroupInfo
import com.ffii.fpsms.modules.pickOrder.web.models.GetPickOrderInfoResponse
@RestController
@RequestMapping("/pickOrder")
class PickOrderController(
@@ -217,8 +217,27 @@ class PickOrderController(
fun createNewGroups(@Valid @RequestBody request: SavePickOrderGroupRequest): MessageResponse {
return pickOrderService.createNewGroups(request)
}
@GetMapping("/all-lots-with-details")
fun getAllPickOrderLotsWithDetails(@RequestParam(required = false) userId: Long?): List<Map<String, Any>> {
return pickOrderService.getAllPickOrderLotsWithDetails(userId)


@GetMapping("/detail-optimized/{userId}")
fun getPickOrderDetailsOptimizedByUser(@PathVariable userId: Long): GetPickOrderInfoResponse {
return pickOrderService.getPickOrderDetailsOptimizedByUser(userId)
}
@PostMapping("/auto-assign-release/{userId}")
fun autoAssignAndReleasePickOrder(@PathVariable userId: Long): MessageResponse {
return pickOrderService.autoAssignAndReleasePickOrder(userId)
}
@GetMapping("/check-pick-completion/{userId}")
fun checkPickOrderCompletion(@PathVariable userId: Long): MessageResponse {
return pickOrderService.checkPickOrderCompletion(userId)
}
@PostMapping("/check-complete/{consoCode}")
fun checkAndCompletePickOrderByConsoCode(@PathVariable consoCode: String): MessageResponse {
return pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode)
}
@GetMapping("/all-lots-with-details/{userId}")
fun getAllPickOrderLotsWithDetails(@PathVariable userId: Long): List<Map<String, Any?>> {
return pickOrderService.getAllPickOrderLotsWithDetailsWithAutoAssign(userId)
}

}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt 查看文件

@@ -32,6 +32,7 @@ data class ReleasePickOrderInfo(
data class GetPickOrderInfo(
val id: Long?,
val code: String?,
val consoCode: String?,
val targetDate: LocalDateTime?,
val type: String?,
val status: String?,


+ 26
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt 查看文件

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

import java.math.BigDecimal
import java.time.LocalDate

data class PickExecutionIssueRequest(
val pickOrderId: Long,
val pickOrderCode: String,
val pickOrderCreateDate: LocalDate? = null,
val pickExecutionDate: LocalDate? = null,
val pickOrderLineId: Long,
val itemId: Long,
val itemCode: String? = null,
val itemDescription: String? = null,
val lotId: Long? = null,
val lotNo: String? = null,
val storeLocation: String? = null,
val requiredQty: BigDecimal? = null,
val actualPickQty: BigDecimal? = null,
val missQty: BigDecimal = BigDecimal.ZERO,
val badItemQty: BigDecimal = BigDecimal.ZERO,
val issueRemark: String? = null,
val pickerName: String? = null,
val handledBy: Long? = null
)

+ 10
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/FailInventoryLotLineRepository.kt 查看文件

@@ -0,0 +1,10 @@
package com.ffii.fpsms.modules.stock.entity

import com.ffii.core.support.AbstractRepository
import org.springframework.stereotype.Repository
import java.io.Serializable

@Repository
interface FailInventoryLotLineRepository: AbstractRepository<FailInventoryLotLine, Long> {
fun findByStockOutLineIdAndDeletedFalse(stockOutLineId: Int): FailInventoryLotLine?
}

+ 47
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/Failinventorylotline.kt 查看文件

@@ -0,0 +1,47 @@
// FPSMS-backend/src/main/java/com/ffii/fpsms/modules/stock/entity/FailInventoryLotLine.kt
package com.ffii.fpsms.modules.stock.entity

import com.fasterxml.jackson.annotation.JsonBackReference
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import com.ffii.core.entity.BaseEntity
import com.ffii.fpsms.modules.master.entity.Items
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Size
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
import java.time.LocalDate
import java.time.LocalDateTime
import java.math.BigDecimal
import com.ffii.fpsms.modules.user.entity.User

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator::class, property = "id")
@Entity
@Table(name = "fail_inventory_lot_line")
open class FailInventoryLotLine : BaseEntity<Long>() {
@Column(name = "Lot_id")
open var Lot_id: Int? = null
@Column(name = "stock_out_line_id")
open var stockOutLineId: Int? = null
@Column(name = "handerId")
open var handlerId: Int? = null
@Column(name = "type")
open var type: String? = null
@Column(name = "qty")
open var qty: BigDecimal? = null
@Column(name = "recordDate")
open var recordDate: LocalDate? = null
@Column(name = "category")
open var category: String? = null
@Column(name = "releasedBy")
open var releasedBy: Int? = null
}

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt 查看文件

@@ -8,6 +8,7 @@ import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import java.io.Serializable
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus

@Repository
interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long> {
@@ -34,4 +35,6 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long

fun findAllByInventoryLotId(id: Serializable): List<InventoryLotLine>
fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: String): List<InventoryLotLine>
fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: InventoryLotLineStatus): List<InventoryLotLine>

}

+ 8
- 3
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt 查看文件

@@ -140,12 +140,17 @@ open class InventoryLotLineService(
}
// Calculate onHoldQty (sum of holdQty from available lots only)
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, "available")
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE.value)
.sumOf { it.holdQty ?: BigDecimal.ZERO }
// Calculate unavailableQty (sum of inQty from unavailable lots only)
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, "unavailable")
.sumOf { it.inQty ?: BigDecimal.ZERO }
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE.value)
.sumOf {
val inQty = it.inQty ?: BigDecimal.ZERO
val outQty = it.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.minus(outQty)
remainingQty
}
// Update the inventory table
val inventory = inventoryRepository.findByItemId(itemId).orElse(null)


+ 17
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt 查看文件

@@ -68,6 +68,11 @@ open class StockOutLineService(
@Transactional
open fun create(request: CreateStockOutLineRequest): MessageResponse {
// pick flow step 1
println("=== DEBUG: create StockOutLine ===")
println("Request consoCode: ${request.consoCode}")
println("Request pickOrderLineId: ${request.pickOrderLineId}")
println("Request inventoryLotLineId: ${request.inventoryLotLineId}")
println("Request qty: ${request.qty}")
// println(request.pickOrderLineId)
val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
@@ -86,7 +91,13 @@ val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInvent
entity = null,
)
}
val allStockOuts = stockOutRepository.findAll()
println("=== DEBUG: All StockOut records ===")
allStockOuts.forEach { stockOut ->
println("StockOut ID: ${stockOut.id}, consoPickOrderCode: ${stockOut.consoPickOrderCode}")
}
val stockOut = stockOutRepository.findByConsoPickOrderCode(request.consoCode).orElseThrow()
println("Found stockOut: ${stockOut.id} with consoCode: ${stockOut.consoPickOrderCode}")
val pickOrderLine = pickOrderLineRepository.saveAndFlush(
pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow()
.apply {
@@ -467,7 +478,12 @@ private fun updateInventoryTableAfterLotRejection(inventoryLotLine: InventoryLot
// Calculate unavailableQty (sum of inQty from unavailable lots only)
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId,InventoryLotLineStatus.UNAVAILABLE.value)
.sumOf { it.inQty ?: BigDecimal.ZERO }
.sumOf {
val inQty = it.inQty ?: BigDecimal.ZERO
val outQty = it.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.minus(outQty)
remainingQty
}
println("Calculated onHoldQty: $onHoldQty")
println("Calculated unavailableQty: $unavailableQty")


+ 497
- 67
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt 查看文件

@@ -22,7 +22,10 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import java.math.RoundingMode
import com.ffii.fpsms.modules.stock.entity.InventoryRepository

import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus
import com.ffii.fpsms.modules.stock.web.model.PickAnotherLotRequest
import com.ffii.fpsms.modules.stock.service.InventoryLotLineService
import com.ffii.fpsms.modules.stock.entity.FailInventoryLotLineRepository
@Service
open class SuggestedPickLotService(
val suggestedPickLotRepository: SuggestPickLotRepository,
@@ -32,7 +35,8 @@ open class SuggestedPickLotService(
val inventoryLotLineService: InventoryLotLineService,
val itemUomService: ItemUomService,
val pickOrderRepository: PickOrderRepository,
val inventoryRepository: InventoryRepository
val inventoryRepository: InventoryRepository,
val failInventoryLotLineRepository: FailInventoryLotLineRepository
) {
// Calculation Available Qty / Remaining Qty
open fun calculateRemainingQtyForInfo(inventoryLotLine: InventoryLotLineInfo?): BigDecimal {
@@ -74,10 +78,10 @@ open class SuggestedPickLotService(
val zero = BigDecimal.ZERO
val one = BigDecimal.ONE
val today = LocalDate.now()
val suggestedList: MutableList<SuggestedPickLot> = mutableListOf()
val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap
// get current inventory lot line qty & grouped by item id
val availableInventoryLotLines = inventoryLotLineService
.allInventoryLotLinesByItemIdIn(itemIds)
@@ -86,21 +90,34 @@ open class SuggestedPickLotService(
.filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today)}
.sortedBy { it.expiryDate }
.groupBy { it.item?.id }
// loop for suggest pick lot line
pols.forEach { line ->
val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }
val lotLines = availableInventoryLotLines[line.item?.id].orEmpty()
val ratio = one // (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP)
// ✅ 修复:remainingQty 应该是销售单位,不需要乘以 ratio
var remainingQty = line.qty ?: zero
println("remaining1 $remainingQty (sales units)")
// ✅ FIX: Calculate remaining quantity needed (not the full required quantity)
val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(line.id!!)
val totalPickedQty = stockOutLines // Exclude rejected lines
.sumOf { it.qty ?: zero }
val requiredQty = line.qty ?: zero
val remainingQty = requiredQty.minus(totalPickedQty)
println("=== SUGGESTION DEBUG for Pick Order Line ${line.id} ===")
println("Required qty: $requiredQty")
println("Total picked qty: $totalPickedQty")
println("Remaining qty needed: $remainingQty")
println("Stock out lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}")
// ✅ FIX: Use remainingQty instead of line.qty
var remainingQtyToAllocate = remainingQty
println("remaining1 $remainingQtyToAllocate (sales units)")
val updatedLotLines = mutableListOf<InventoryLotLineInfo>()

lotLines.forEachIndexed { index, lotLine ->
if (remainingQty <= zero) return@forEachIndexed

if (remainingQtyToAllocate <= zero) return@forEachIndexed
println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}")
// ✅ 修复:计算可用数量,转换为销售单位
@@ -111,7 +128,7 @@ open class SuggestedPickLotService(
.divide(ratio, 2, RoundingMode.HALF_UP)
println("holdQtyMap[lotLine.id] ?: zero ${holdQtyMap[lotLine.id] ?: zero}")
if (availableQtyInSalesUnits <= zero) {
updatedLotLines += lotLine
return@forEachIndexed
@@ -119,10 +136,10 @@ open class SuggestedPickLotService(
println("$index : ${lotLine.id}")
val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() }
val originalHoldQty = inventoryLotLine?.holdQty
// ✅ 修复:在销售单位中计算分配数量
val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQty)
remainingQty = remainingQty.minus(assignQtyInSalesUnits)
val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQtyToAllocate)
remainingQtyToAllocate = remainingQtyToAllocate.minus(assignQtyInSalesUnits)
val newHoldQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero
// ✅ 修复:将销售单位转换为基础单位来更新 holdQty
val assignQtyInBaseUnits = assignQtyInSalesUnits.multiply(ratio)
@@ -135,15 +152,15 @@ open class SuggestedPickLotService(
qty = assignQtyInSalesUnits // ✅ 保存销售单位
}
}
// if still have remainingQty
println("remaining2 $remainingQty (sales units)")
if (remainingQty > zero) {
println("remaining2 $remainingQtyToAllocate (sales units)")
if (remainingQtyToAllocate > zero) {
suggestedList += SuggestedPickLot().apply {
type = SuggestedPickLotType.PICK_ORDER
suggestedLotLine = null
pickOrderLine = line
qty = remainingQty // ✅ 保存销售单位
qty = remainingQtyToAllocate // ✅ 保存销售单位
}
}
}
@@ -206,78 +223,241 @@ open class SuggestedPickLotService(
try {
val pickOrder = pickOrderRepository.findById(pickOrderId).orElseThrow()
println("=== RESUGGEST DEBUG START ===")
println("Pick Order ID: $pickOrderId")
println("Pick Order Code: ${pickOrder.code}")
println("Pick Order Status: ${pickOrder.status}")
// ✅ NEW: Get ALL pick orders for the same items
val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct()
println("Item IDs in current pick order: $itemIds")
val allCompetingPickOrders = mutableListOf<PickOrder>()
itemIds.forEach { itemId ->
val competingOrders = pickOrderLineRepository.findAllPickOrdersByItemId(itemId)
.filter { it.id != pickOrderId } // Exclude current pick order
println("Found ${competingOrders.size} competing pick orders for item $itemId")
competingOrders.forEach { order ->
println(" - Competing Order: ${order.code} (ID: ${order.id}, Status: ${order.status})")
}
allCompetingPickOrders.addAll(competingOrders)
}
// ✅ NEW: Resuggest ALL competing pick orders together
val allPickOrdersToResuggest = listOf(pickOrder) + allCompetingPickOrders
// ✅ FIX: Only resuggest pick orders that have rejected stock out lines
val allPickOrdersToResuggest = (listOf(pickOrder) + allCompetingPickOrders)
.filter { pickOrderToCheck ->
// Only resuggest if the pick order has rejected stock out lines
pickOrderToCheck.pickOrderLines.any { pol ->
val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!)
val hasRejectedStockOutLine = stockOutLines.any { it.status == "rejected" }
if (hasRejectedStockOutLine) {
println("Pick Order ${pickOrderToCheck.code} has rejected stock out lines - will resuggest")
stockOutLines.filter { it.status == "rejected" }.forEach { sol ->
println(" - Rejected stock out line: ${sol.id} (lot: ${sol.inventoryLotLineId}, qty: ${sol.qty})")
}
}
hasRejectedStockOutLine
}
}
// ✅ FIX: Only clear suggestions for competing pick orders, NOT all lots
val allPickOrderIds = allPickOrdersToResuggest.mapNotNull { it.id }
val allSuggestions = findAllSuggestionsForPickOrders(allPickOrderIds)
println("=== RESUGGEST DEBUG ===")
println("Original pick orders: ${(listOf(pickOrder) + allCompetingPickOrders).size}")
println("Filtered pick orders to resuggest: ${allPickOrdersToResuggest.size}")
println("Pick orders being resuggested: ${allPickOrdersToResuggest.map { "${it.code}(${it.status})" }}")
// ✅ FIX: Only clear holdQty for lots that are currently suggested
val currentlySuggestedLotIds = allSuggestions.mapNotNull { suggestion -> suggestion.suggestedLotLine?.id }.distinct()
val currentlySuggestedLots = inventoryLotLineRepository.findAllByIdIn(currentlySuggestedLotIds)
// ✅ FIX: Only resuggest if there are actually pick orders with rejected lots
if (allPickOrdersToResuggest.isEmpty()) {
println("No pick orders need resuggesting - no rejected lots found")
return MessageResponse(
id = pickOrderId,
name = "No resuggest needed",
code = "SUCCESS",
type = "resuggest",
message = "No pick orders have rejected lots, no resuggest needed",
errorPosition = null
)
}
println("=== RESUGGEST DEBUG ===")
println("Currently suggested lot IDs: $currentlySuggestedLotIds")
println("Total competing pick orders: ${allPickOrdersToResuggest.size}")
// ✅ FIX: Get all pick order line IDs for the orders to resuggest
val allPickOrderLineIds = allPickOrdersToResuggest
.flatMap { it.pickOrderLines }
.mapNotNull { it.id }
// ✅ FIX: Only reset holdQty for currently suggested lots
currentlySuggestedLots.forEach { lotLine ->
println("Clearing holdQty for currently suggested lot line ${lotLine.id}: ${lotLine.holdQty} -> 0")
lotLine.holdQty = BigDecimal.ZERO
println("All pick order line IDs to resuggest: $allPickOrderLineIds")
// ✅ FIX: Get all existing suggestions for these pick order lines
val allSuggestions = suggestedPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIds)
println("Found ${allSuggestions.size} existing suggestions")
// ✅ FIX: Separate suggestions to keep (those with rejected stock out lines) and delete
val suggestionsToKeep = allSuggestions.filter { suggestion ->
val pickOrderLineId = suggestion.pickOrderLine?.id
val suggestedLotLineId = suggestion.suggestedLotLine?.id
println("Checking suggestion ${suggestion.id}: pickOrderLineId=$pickOrderLineId, suggestedLotLineId=$suggestedLotLineId")
if (pickOrderLineId != null && suggestedLotLineId != null) {
val stockOutLines = stockOutLIneRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
pickOrderLineId,
suggestedLotLineId
)
val hasRejectedStockOutLine = stockOutLines.any { it.status == "rejected" }
println(" Stock out lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}")
println(" Has rejected stock out line: $hasRejectedStockOutLine")
hasRejectedStockOutLine
} else {
println(" Missing pickOrderLineId or suggestedLotLineId")
false
}
}
val suggestionsToDelete = allSuggestions.filter { suggestion ->
!suggestionsToKeep.contains(suggestion)
}
inventoryLotLineRepository.saveAllAndFlush(currentlySuggestedLots)
// Delete ALL suggestions for all competing pick orders
suggestedPickLotRepository.deleteAllById(allSuggestions.mapNotNull { suggestion -> suggestion.id })
println("Suggestions to keep (with rejected stock out lines): ${suggestionsToKeep.size}")
println("Suggestions to delete: ${suggestionsToDelete.size}")
// ✅ NEW: Generate optimal suggestions for ALL pick orders together
val newSuggestions = generateOptimalSuggestionsForAllPickOrders(allPickOrdersToResuggest, emptyMap())
// ✅ FIX: Clear holdQty ONLY for lots that have rejected stock out lines
val rejectedLotIds = suggestionsToKeep.mapNotNull { it.suggestedLotLine?.id }.distinct()
println("Rejected lot IDs: $rejectedLotIds")
// Save new suggestions and update holdQty
val savedSuggestions = suggestedPickLotRepository.saveAllAndFlush(newSuggestions)
rejectedLotIds.forEach { lotId ->
val lot = inventoryLotLineRepository.findById(lotId).orElse(null)
lot?.let {
val originalHoldQty = it.holdQty ?: BigDecimal.ZERO
it.holdQty = BigDecimal.ZERO
inventoryLotLineRepository.save(it)
println("Cleared holdQty for rejected lot ${lot.id}: $originalHoldQty -> 0")
}
}
// ✅ FIX: Update holdQty for newly suggested lots
val newlySuggestedLotIds = savedSuggestions.mapNotNull { suggestion -> suggestion.suggestedLotLine?.id }.distinct()
val newlySuggestedLots = inventoryLotLineRepository.findAllByIdIn(newlySuggestedLotIds)
// ✅ NEW: Reduce holdQty for lots that are no longer suggested
val deletedSuggestions = suggestionsToDelete
val deletedLotIds = deletedSuggestions.mapNotNull { it.suggestedLotLine?.id }.distinct()
println("Deleted lot IDs: $deletedLotIds")
savedSuggestions.forEach { suggestion ->
val lotLine = newlySuggestedLots.find { it.id == suggestion.suggestedLotLine?.id }
lotLine?.let {
val ratio = BigDecimal.ONE
val suggestionQtyInBaseUnits = (suggestion.qty ?: BigDecimal.ZERO).multiply(ratio)
println("Setting holdQty for newly suggested lot line ${it.id}: ${it.holdQty} -> ${(it.holdQty ?: BigDecimal.ZERO).plus(suggestionQtyInBaseUnits)}")
it.holdQty = (it.holdQty ?: BigDecimal.ZERO).plus(suggestionQtyInBaseUnits)
deletedLotIds.forEach { lotId ->
val lot = inventoryLotLineRepository.findById(lotId).orElse(null)
lot?.let {
val originalHoldQty = it.holdQty ?: BigDecimal.ZERO
val deletedQty = deletedSuggestions
.filter { it.suggestedLotLine?.id == lotId }
.sumOf { it.qty ?: BigDecimal.ZERO }
val newHoldQty = originalHoldQty.minus(deletedQty)
it.holdQty = if (newHoldQty < BigDecimal.ZERO) BigDecimal.ZERO else newHoldQty
inventoryLotLineRepository.save(it)
println("Reduced holdQty for deleted lot ${lot.id}: $originalHoldQty - $deletedQty = ${it.holdQty}")
}
}
inventoryLotLineRepository.saveAllAndFlush(newlySuggestedLots)
// ✅ FIX: Delete only the suggestions that should be deleted
if (suggestionsToDelete.isNotEmpty()) {
suggestedPickLotRepository.deleteAll(suggestionsToDelete)
println("Deleted ${suggestionsToDelete.size} suggestions")
}
// ✅ NEW: Build holdQtyMap with existing holdQty from other pick orders
val existingHoldQtyMap = mutableMapOf<Long?, BigDecimal?>()
// Get all lots that are being used by other pick orders (including those NOT being resuggested)
val allOtherPickOrderLineIds = allCompetingPickOrders
.flatMap { it.pickOrderLines }
.mapNotNull { it.id }
println("Other pick order line IDs: $allOtherPickOrderLineIds")
if (allOtherPickOrderLineIds.isNotEmpty()) {
val otherSuggestions = suggestedPickLotRepository.findAllByPickOrderLineIdIn(allOtherPickOrderLineIds)
println("Found ${otherSuggestions.size} other suggestions")
otherSuggestions.forEach { suggestion ->
val lotId = suggestion.suggestedLotLine?.id
val qty = suggestion.qty
println("Processing other suggestion ${suggestion.id}: lotId=$lotId, qty=$qty")
if (lotId != null && qty != null) {
val currentHoldQty = existingHoldQtyMap[lotId] ?: BigDecimal.ZERO
val newHoldQty = currentHoldQty.plus(qty)
existingHoldQtyMap[lotId] = newHoldQty
println(" Updated holdQty for lot $lotId: $currentHoldQty + $qty = $newHoldQty")
}
}
}
println("Final existing holdQtyMap: $existingHoldQtyMap")
// ✅ FIX: Create new suggestions for all pick orders to resuggest
allPickOrdersToResuggest.forEach { pickOrderToResuggest ->
println("=== Creating new suggestions for pick order: ${pickOrderToResuggest.code} ===")
val request = SuggestedPickLotForPolRequest(
pickOrderLines = pickOrderToResuggest.pickOrderLines,
holdQtyMap = existingHoldQtyMap.toMutableMap() // ✅ Use existing holdQty
)
val response = suggestionForPickOrderLines(request)
println("Generated ${response.suggestedList.size} new suggestions")
response.suggestedList.forEach { suggestion ->
println(" - Suggestion: lotId=${suggestion.suggestedLotLine?.id}, qty=${suggestion.qty}")
}
// ✅ FIX: Save the suggestions to the database
if (response.suggestedList.isNotEmpty()) {
val savedSuggestions = suggestedPickLotRepository.saveAllAndFlush(response.suggestedList)
println("Saved ${savedSuggestions.size} new suggestions for pick order: ${pickOrderToResuggest.code}")
// ✅ FIX: Update holdQty for the lots that were suggested - CUMULATIVE
response.holdQtyMap.forEach { (lotId, newHoldQty) ->
if (lotId != null && newHoldQty != null && newHoldQty > BigDecimal.ZERO) {
val lot = inventoryLotLineRepository.findById(lotId).orElse(null)
lot?.let {
val currentHoldQty = it.holdQty ?: BigDecimal.ZERO
val existingHoldQty = existingHoldQtyMap[lotId] ?: BigDecimal.ZERO
// ✅ FIX: Calculate the additional holdQty needed
val additionalHoldQty = newHoldQty.minus(existingHoldQty)
val finalHoldQty = currentHoldQty.plus(additionalHoldQty)
it.holdQty = finalHoldQty
inventoryLotLineRepository.save(it)
// ✅ Update the existingHoldQtyMap for next iteration
existingHoldQtyMap[lotId] = newHoldQty
println("Updated holdQty for lot $lotId: $currentHoldQty + $additionalHoldQty = $finalHoldQty")
}
}
}
} else {
println("No suggestions generated for pick order: ${pickOrderToResuggest.code}")
}
}
// ✅ FIX: Update inventory table for each pick order
allPickOrdersToResuggest.forEach { pickOrderToUpdate ->
println("=== Updating inventory table for pick order: ${pickOrderToUpdate.code} ===")
updateInventoryTableAfterResuggest(pickOrderToUpdate)
}
println("=== RESUGGEST COMPLETED ===")
println("=== RESUGGEST DEBUG END ===")
return MessageResponse(
id = pickOrderId,
name = "Pick order resuggested successfully",
code = "SUCCESS",
type = "resuggest",
message = "Redistributed suggestions for ${allPickOrdersToResuggest.size} competing pick orders following FEFO order",
message = "Resuggested ${allPickOrdersToResuggest.size} pick orders",
errorPosition = null
)
} catch (e: Exception) {
println("=== RESUGGEST ERROR ===")
println("Error in resuggestPickOrder: ${e.message}")
e.printStackTrace()
return MessageResponse(
id = pickOrderId,
id = null,
name = "Failed to resuggest pick order",
code = "ERROR",
type = "resuggest",
@@ -286,7 +466,6 @@ open class SuggestedPickLotService(
)
}
}

private fun findAllSuggestionsForPickOrders(pickOrderIds: List<Long>): List<SuggestedPickLot> {
val allPickOrderLines = mutableListOf<PickOrderLine>()
@@ -336,9 +515,30 @@ private fun generateOptimalSuggestionsForAllPickOrders(
val lotEntities = inventoryLotLineRepository.findAllByIdIn(availableLots.mapNotNull { it.id })
lotEntities.forEach { lot -> lot.holdQty = BigDecimal.ZERO }
// ✅ FIX: Allocate lots directly to specific pick order lines (FEFO order)
val remainingPickOrderLines = pickOrderLines.toMutableList()
val remainingQtyPerLine = pickOrderLines.associate { it.id to (it.qty ?: zero) }.toMutableMap()
// ✅ FIX: Calculate remaining quantity for each pick order line
// ✅ FIX: Calculate remaining quantity for each pick order line
val remainingQtyPerLine = pickOrderLines.associate { pol ->
val stockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!)
// ✅ FIX: Count picked qty from ALL statuses except 'rejected'
// This includes 'completed', 'pending', 'checked', 'partially_completed'
val totalPickedQty = stockOutLines
.sumOf { it.qty ?: BigDecimal.ZERO }
val requiredQty = pol.qty ?: zero
val remainingQty = requiredQty.minus(totalPickedQty)
println("Pick Order Line ${pol.id}: required=${requiredQty}, picked=${totalPickedQty}, remaining=${remainingQty}")
println("Stock Out Lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}")
pol.id to remainingQty
}.toMutableMap()
// ✅ FIX: Filter out pick order lines that don't need more qty
val remainingPickOrderLines = pickOrderLines.filter { pol ->
val remainingQty = remainingQtyPerLine[pol.id] ?: zero
remainingQty > zero
}.toMutableList()
lotEntities.forEach { lot ->
if (remainingPickOrderLines.isEmpty()) return@forEach
@@ -401,13 +601,18 @@ private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) {
val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct()
itemIds.forEach { itemId ->
// ✅ FIX: Use .value to get string representation
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE.value)
// ✅ FIX: Calculate onHoldQty for ALL pick orders that use this item, not just the current one
val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
.sumOf { it.holdQty ?: BigDecimal.ZERO }
// ✅ FIX: Use .value to get string representation
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE.value)
.sumOf { it.inQty ?: BigDecimal.ZERO }
// ✅ FIX: Use enum method instead of string method
val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE)
.sumOf {
val inQty = it.inQty ?: BigDecimal.ZERO
val outQty = it.outQty ?: BigDecimal.ZERO
val remainingQty = inQty.minus(outQty)
remainingQty
}
// Update the inventory table
val inventory = inventoryRepository.findByItemId(itemId).orElse(null)
@@ -416,7 +621,7 @@ private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) {
inventory.unavailableQty = unavailableQty
inventoryRepository.save(inventory)
println("Updated inventory for item $itemId after resuggest: onHoldQty=$onHoldQty, unavailableQty=$unavailableQty")
println("Updated inventory for item $itemId: onHoldQty=$onHoldQty, unavailableQty=$unavailableQty")
}
}
} catch (e: Exception) {
@@ -737,6 +942,231 @@ private fun generateCorrectSuggestionsWithOriginalHolds(

return suggestions
}
// 在 SuggestedPickLotService.kt 的 recordFailInventoryLotLine 方法中修复:

// 在 SuggestedPickLotService.kt 中修改 recordFailInventoryLotLine 方法:

// 在 SuggestedPickLotService.kt 中修改 recordFailInventoryLotLine 方法:

@Transactional(rollbackFor = [Exception::class])
open fun recordFailInventoryLotLine(request: PickAnotherLotRequest): MessageResponse {
try {
// 获取相关的库存批次行
val inventoryLotLine = inventoryLotLineRepository.findById(request.lotId).orElseThrow()
// 获取相关的拣货订单行
val pickOrderLine = pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow()
// 获取相关的出库行(如果存在)
val stockOutLine = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(request.pickOrderLineId)
.find { it.inventoryLotLineId == request.lotId }
// 创建失败记录
val failRecord = FailInventoryLotLine().apply {
this.Lot_id = inventoryLotLine.inventoryLot?.id?.toInt()
this.stockOutLineId = stockOutLine?.id?.toInt()
this.handlerId = request.handlerId?.toInt()
this.type = request.type
this.qty = request.qty
this.recordDate = request.recordDate ?: LocalDate.now()
this.category = request.category
this.releasedBy = request.releasedBy?.toInt()
}
val savedRecord = failInventoryLotLineRepository.save(failRecord)
// 根据失败类型执行不同的逻辑
when (request.type) {
"missing" -> {
// 类型为 missing:减少库存批次行的可用数量
handleMissingItem(inventoryLotLine, request.qty)
}
"item_broken" -> {
// 类型为 item_broken:将批次标记为不可用,更新库存表
handleBrokenItem(inventoryLotLine, request.qty)
}
else -> {
// 其他类型:只记录失败,不执行特殊逻辑
println("Unknown fail type: ${request.type}")
}
}
// 重新建议拣货批次
val pickOrder = pickOrderLine.pickOrder
if (pickOrder != null) {
resuggestPickOrder(pickOrder.id!!)
}
return MessageResponse(
id = savedRecord.id,
name = "Fail inventory lot line recorded successfully",
code = "SUCCESS",
type = "fail_inventory_lot_line",
message = "Fail inventory lot line recorded and resuggested successfully",
errorPosition = null
)
} catch (e: Exception) {
return MessageResponse(
id = null,
name = "Failed to record fail inventory lot line",
code = "ERROR",
type = "fail_inventory_lot_line",
message = "Error: ${e.message}",
errorPosition = null
)
}
}

// 处理缺失物品的情况
private fun handleMissingItem(inventoryLotLine: InventoryLotLine, failQty: BigDecimal) {
try {
// 减少库存批次行的可用数量
val currentInQty = inventoryLotLine.inQty ?: BigDecimal.ZERO
val newInQty = currentInQty.minus(failQty)
// 确保不会变成负数
inventoryLotLine.inQty = if (newInQty < BigDecimal.ZERO) BigDecimal.ZERO else newInQty
inventoryLotLineRepository.save(inventoryLotLine)
println("Missing item handled: Reduced inQty by $failQty for lot line ${inventoryLotLine.id}")
} catch (e: Exception) {
println("Error handling missing item: ${e.message}")
throw e
}
}

// 处理损坏物品的情况
private fun handleBrokenItem(inventoryLotLine: InventoryLotLine, failQty: BigDecimal) {
try {
// 1. 将库存批次行标记为不可用
inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE
// 2. 更新库存表的不可用数量
val itemId = inventoryLotLine.inventoryLot?.item?.id
if (itemId != null) {
val inventory = inventoryRepository.findByItemId(itemId).orElse(null)
if (inventory != null) {
val currentUnavailableQty = inventory.unavailableQty ?: BigDecimal.ZERO
inventory.unavailableQty = currentUnavailableQty.plus(failQty)
inventoryRepository.save(inventory)
println("Broken item handled: Updated unavailableQty by $failQty for item $itemId")
}
}
// 3. 保存库存批次行的状态变更
inventoryLotLineRepository.save(inventoryLotLine)
println("Broken item handled: Marked lot line ${inventoryLotLine.id} as unavailable")
} catch (e: Exception) {
println("Error handling broken item: ${e.message}")
throw e
}
}

// ... existing code ...

@Transactional(rollbackFor = [Exception::class])
open fun updateSuggestedLotLineId(suggestedPickLotId: Long, newLotLineId: Long): MessageResponse {
try {
println("=== DEBUG: updateSuggestedLotLineId ===")
println("suggestedPickLotId: $suggestedPickLotId")
println("newLotLineId: $newLotLineId")

// Get the existing suggested pick lot
val suggestedPickLot = suggestedPickLotRepository.findById(suggestedPickLotId).orElse(null)
if (suggestedPickLot == null) {
return MessageResponse(
id = null,
name = "Suggested pick lot not found",
code = "ERROR",
type = "suggested_pick_lot",
message = "Suggested pick lot with ID $suggestedPickLotId not found",
errorPosition = null
)
}

// Get the new inventory lot line
val newInventoryLotLine = inventoryLotLineRepository.findById(newLotLineId).orElse(null)
if (newInventoryLotLine == null) {
return MessageResponse(
id = null,
name = "Inventory lot line not found",
code = "ERROR",
type = "inventory_lot_line",
message = "Inventory lot line with ID $newLotLineId not found",
errorPosition = null
)
}

// Validate that the new lot line has the same item as the pick order line
val pickOrderLine = suggestedPickLot.pickOrderLine
if (pickOrderLine?.item?.id != newInventoryLotLine.inventoryLot?.item?.id) {
return MessageResponse(
id = null,
name = "Item mismatch",
code = "ERROR",
type = "validation",
message = "The new lot line belongs to a different item than the pick order line",
errorPosition = null
)
}

// Get the old inventory lot line for hold quantity management
val oldInventoryLotLine = suggestedPickLot.suggestedLotLine
val suggestedQty = suggestedPickLot.qty ?: BigDecimal.ZERO

// Update hold quantities
if (oldInventoryLotLine != null) {
// Reduce hold quantity from old lot
val oldHoldQty = oldInventoryLotLine.holdQty ?: BigDecimal.ZERO
val newOldHoldQty = oldHoldQty.minus(suggestedQty)
oldInventoryLotLine.holdQty = if (newOldHoldQty < BigDecimal.ZERO) BigDecimal.ZERO else newOldHoldQty
inventoryLotLineRepository.save(oldInventoryLotLine)
println("Reduced holdQty for old lot ${oldInventoryLotLine.id}: $oldHoldQty -> ${oldInventoryLotLine.holdQty}")
}

// Add hold quantity to new lot
val newHoldQty = (newInventoryLotLine.holdQty ?: BigDecimal.ZERO).plus(suggestedQty)
newInventoryLotLine.holdQty = newHoldQty
inventoryLotLineRepository.save(newInventoryLotLine)
println("Added holdQty for new lot ${newInventoryLotLine.id}: ${newInventoryLotLine.holdQty}")

// Update the suggested pick lot
suggestedPickLot.suggestedLotLine = newInventoryLotLine
val savedSuggestedPickLot = suggestedPickLotRepository.save(suggestedPickLot)

println("✅ Successfully updated suggested pick lot ${suggestedPickLotId} to use lot line ${newLotLineId}")

return MessageResponse(
id = savedSuggestedPickLot.id,
name = "Suggested pick lot updated successfully",
code = "SUCCESS",
type = "suggested_pick_lot",
message = "Successfully updated suggested pick lot to use new lot line",
errorPosition = null
)

} catch (e: Exception) {
println("❌ Error in updateSuggestedLotLineId: ${e.message}")
e.printStackTrace()
return MessageResponse(
id = null,
name = "Failed to update suggested pick lot",
code = "ERROR",
type = "suggested_pick_lot",
message = "Failed to update suggested pick lot: ${e.message}",
errorPosition = null
)
}
}

// ... existing code ...

}



+ 14
- 1
src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt 查看文件

@@ -15,7 +15,9 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickOrder
import com.ffii.fpsms.modules.stock.web.model.SuggestedPickLotForPolRequest
import kotlin.jvm.optionals.getOrDefault
import kotlin.jvm.optionals.getOrNull

import org.springframework.web.bind.annotation.RequestBody
import com.ffii.fpsms.modules.stock.web.model.PickAnotherLotRequest
import com.ffii.fpsms.modules.stock.web.model.UpdateSuggestedLotLineIdRequest
@RequestMapping("/suggestedPickLot")
@RestController
class SuggestedPickLotController(
@@ -36,4 +38,15 @@ class SuggestedPickLotController(
fun resuggestPickOrder(@PathVariable pickOrderId: Long): MessageResponse {
return suggestedPickLotService.resuggestPickOrder(pickOrderId)
}
@PostMapping("/recordFailLot")
fun recordFailInventoryLotLine(@RequestBody request: PickAnotherLotRequest): MessageResponse {
return suggestedPickLotService.recordFailInventoryLotLine(request)
}
@PostMapping("/update-suggested-lot/{suggestedPickLotId}")
fun updateSuggestedLotLineId(
@PathVariable suggestedPickLotId: Long,
@RequestBody request: UpdateSuggestedLotLineIdRequest
): MessageResponse {
return suggestedPickLotService.updateSuggestedLotLineId(suggestedPickLotId, request.newLotLineId)
}
}

+ 12
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/PickAnotherLotRequest.kt 查看文件

@@ -0,0 +1,12 @@
package com.ffii.fpsms.modules.stock.web.model
import java.time.LocalDate
data class PickAnotherLotRequest(
val pickOrderLineId: Long,
val lotId: Long,
val qty: java.math.BigDecimal,
val type: String, // "missing" or "item_broken"
val handlerId: Long? = null, // 可选:处理人员ID
val category: String? = null, // 可选:分类/原因
val releasedBy: Long? = null, // 可选:释放人员ID
val recordDate: LocalDate? = null // 可选:记录日期,默认为当前日期
)

+ 5
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/UpdateSuggestedLotLineIdRequest.kt 查看文件

@@ -0,0 +1,5 @@
package com.ffii.fpsms.modules.stock.web.model

data class UpdateSuggestedLotLineIdRequest(
val newLotLineId: Long
)

+ 44
- 0
src/main/resources/db/changelog/changes/20250902_02_enson/01_create_group_enson.sql 查看文件

@@ -0,0 +1,44 @@
--liquibase formatted sql

--changeset enson:update pick_order_group

CREATE TABLE `pick_execution_issue` (
`id` int NOT NULL AUTO_INCREMENT,
`pick_order_id` int NOT NULL,
`pick_order_code` varchar(50) NOT NULL,
`pick_order_create_date` date DEFAULT NULL,
`pick_execution_date` date DEFAULT NULL,
`pick_order_line_id` int NOT NULL,
`item_id` int NOT NULL,
`item_code` varchar(50) DEFAULT NULL,
`item_description` varchar(255) DEFAULT NULL,
`lot_id` int DEFAULT NULL,
`lot_no` varchar(50) DEFAULT NULL,
`store_location` varchar(100) DEFAULT NULL,
`required_qty` decimal(10,2) DEFAULT NULL,
`actual_pick_qty` decimal(10,2) DEFAULT NULL,
`miss_qty` decimal(10,2) DEFAULT '0.00',
`bad_item_qty` decimal(10,2) DEFAULT '0.00',
`issue_remark` text,
`picker_name` varchar(100) DEFAULT NULL,
`handle_status` enum('pending','handled','resolved') DEFAULT 'pending',
`handle_date` date DEFAULT NULL,
`handled_by` int DEFAULT NULL,
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`createdBy` varchar(30) DEFAULT NULL,
`version` int NOT NULL DEFAULT '0',
`modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modifiedBy` varchar(30) DEFAULT NULL,
`deleted` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `FK_PICK_EXECUTION_ISSUE_ON_PICK_ORDER` (`pick_order_id`),
KEY `FK_PICK_EXECUTION_ISSUE_ON_PICK_ORDER_LINE` (`pick_order_line_id`),
KEY `FK_PICK_EXECUTION_ISSUE_ON_ITEM` (`item_id`),
KEY `FK_PICK_EXECUTION_ISSUE_ON_LOT` (`lot_id`),
KEY `FK_PICK_EXECUTION_ISSUE_ON_HANDLED_BY` (`handled_by`),
CONSTRAINT `FK_PICK_EXECUTION_ISSUE_ON_HANDLED_BY` FOREIGN KEY (`handled_by`) REFERENCES `user` (`id`),
CONSTRAINT `FK_PICK_EXECUTION_ISSUE_ON_ITEM` FOREIGN KEY (`item_id`) REFERENCES `items` (`id`),
CONSTRAINT `FK_PICK_EXECUTION_ISSUE_ON_LOT` FOREIGN KEY (`lot_id`) REFERENCES `inventory_lot` (`id`),
CONSTRAINT `FK_PICK_EXECUTION_ISSUE_ON_PICK_ORDER` FOREIGN KEY (`pick_order_id`) REFERENCES `pick_order` (`id`),
CONSTRAINT `FK_PICK_EXECUTION_ISSUE_ON_PICK_ORDER_LINE` FOREIGN KEY (`pick_order_line_id`) REFERENCES `pick_order_line` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

Loading…
取消
儲存