| @@ -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 | |||
| } | |||
| @@ -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,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> | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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?, | |||
| @@ -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 | |||
| ) | |||
| @@ -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? | |||
| } | |||
| @@ -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 | |||
| } | |||
| @@ -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> | |||
| } | |||
| @@ -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) | |||
| @@ -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") | |||
| @@ -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 ... | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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 // 可选:记录日期,默认为当前日期 | |||
| ) | |||
| @@ -0,0 +1,5 @@ | |||
| package com.ffii.fpsms.modules.stock.web.model | |||
| data class UpdateSuggestedLotLineIdRequest( | |||
| val newLotLineId: Long | |||
| ) | |||
| @@ -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 | |||