From 5af3e1bc62d68b696b1922f2918c061ea48368c2 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 15 Sep 2025 15:02:36 +0800 Subject: [PATCH] update --- .../pickOrder/entity/PickExecutionIssue.kt | 131 +++ .../entity/PickExecutionIssueRepository.kt | 16 + .../pickOrder/entity/PickOrderRepository.kt | 9 + .../service/PickExecutionIssueService.kt | 435 +++++++++ .../pickOrder/service/PickOrderService.kt | 898 +++++++++++++++--- .../web/PickExecutionIssueController.kt | 30 + .../pickOrder/web/PickOrderController.kt | 27 +- .../web/models/ConsoPickOrderResponse.kt | 1 + .../web/models/PickExecutionIssueRequest.kt | 26 + .../entity/FailInventoryLotLineRepository.kt | 10 + .../stock/entity/Failinventorylotline.kt | 47 + .../entity/InventoryLotLineRepository.kt | 3 + .../stock/service/InventoryLotLineService.kt | 11 +- .../stock/service/StockOutLineService.kt | 18 +- .../stock/service/SuggestedPickLotService.kt | 564 +++++++++-- .../stock/web/SuggestedPickLotController.kt | 15 +- .../stock/web/model/PickAnotherLotRequest.kt | 12 + .../model/UpdateSuggestedLotLineIdRequest.kt | 5 + .../01_create_group_enson.sql | 44 + 19 files changed, 2115 insertions(+), 187 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/entity/FailInventoryLotLineRepository.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/entity/Failinventorylotline.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/PickAnotherLotRequest.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/UpdateSuggestedLotLineIdRequest.kt create mode 100644 src/main/resources/db/changelog/changes/20250902_02_enson/01_create_group_enson.sql diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssue.kt new file mode 100644 index 0000000..a472991 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt new file mode 100644 index 0000000..3d53c19 --- /dev/null +++ b/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 { + fun findByPickOrderIdAndDeletedFalse(pickOrderId: Long): List + fun findByPickOrderLineIdAndDeletedFalse(pickOrderLineId: Long): List + fun findByLotIdAndDeletedFalse(lotId: Long): List + fun findByPickOrderLineIdAndLotIdAndDeletedFalse( + pickOrderLineId: Long, + lotId: Long + ): List +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt index ead5172..e6994e6 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickOrderRepository.kt +++ b/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 { fun findAllByConsoCodeAndStatus(consoCode: String, status: PickOrderStatus): List fun findAllByIdIn(id: List): List + + @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): List + + @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 + @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): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt new file mode 100644 index 0000000..7598619 --- /dev/null +++ b/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 { + return pickExecutionIssueRepository.findByPickOrderIdAndDeletedFalse(pickOrderId) + } + + open fun getPickExecutionIssuesByPickOrderLine(pickOrderLineId: Long): List { + return pickExecutionIssueRepository.findByPickOrderLineIdAndDeletedFalse(pickOrderLineId) + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt index 3530168..d3d5555 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt @@ -47,7 +47,7 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.jvm.optionals.getOrNull import com.ffii.fpsms.modules.pickOrder.entity.projection.PickOrderGroupInfo - +import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository @Service open class PickOrderService( private val jdbcDao: JdbcDao, @@ -65,13 +65,16 @@ open class PickOrderService( private val uomConversionRepository: UomConversionRepository, private val jobOrderRepository: JobOrderRepository, private val itemUomService: ItemUomService, + private val deliveryOrderRepository: DeliveryOrderRepository, ) : AbstractBaseEntityService(jdbcDao, pickOrderRepository) { open fun create(request: SavePickOrderRequest): MessageResponse { val code = assignPickCode() val jo = request.joId?.let { jobOrderRepository.findById(it).getOrNull() } + val deliveryOrder = request.doId?.let { deliveryOrderRepository.findById(it).getOrNull() } val pickOrder = PickOrder().apply { this.code = code this.jobOrder = jo + this.deliveryOrder = deliveryOrder this.targetDate = request.targetDate.atStartOfDay() this.type = request.type this.status = PickOrderStatus.PENDING @@ -202,6 +205,7 @@ open class PickOrderService( GetPickOrderInfo( id = po.id, code = po.code, + consoCode = po.consoCode, targetDate = po.targetDate, type = po.type?.value, status = po.status?.value, @@ -716,8 +720,6 @@ open class PickOrderService( println("Stock Out Lines: ${stockOutLines.map { "${it.id}(status=${it.status}, qty=${it.qty})" }}") val totalPickedQty = stockOutLines - .filter { it.status in listOf("completed", "COMPLETE", "partially_completed") } // Include partially completed - .filter { (it.qty ?: zero) > BigDecimal.ZERO } // Only lines with actual quantities .sumOf { it.qty ?: zero } println("Total Picked Qty: $totalPickedQty") @@ -744,6 +746,7 @@ open class PickOrderService( GetPickOrderInfo( id = po.id, code = po.code, + consoCode = po.consoCode, targetDate = po.targetDate, type = po.type?.value, status = po.status?.value, @@ -798,6 +801,7 @@ open class PickOrderService( // 重用现有的 getPickOrdersInfo 方法 return getPickOrdersInfo(releasedPickOrderIds) } + open fun getPickOrderLineLotDetails(pickOrderLineId: Long): List> { val today = LocalDate.now() @@ -813,15 +817,17 @@ open class PickOrderService( w.name as location, COALESCE(uc.udfudesc, 'N/A') as stockUnit, (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty, - spl.qty as requiredQty, + COALESCE(spl.qty, 0) as requiredQty, -- ✅ 使用COALESCE处理null值 COALESCE(ill.inQty, 0) as inQty, + COALESCE(ill.outQty, 0) as outQty, + COALESCE(ill.holdQty, 0) as holdQty, COALESCE(sol.qty, 0) as actualPickQty, - spl.id as suggestedPickLotId, + COALESCE(spl.id, 0) as suggestedPickLotId, -- ✅ 使用COALESCE处理null值 ill.status as lotStatus, sol.id as stockOutLineId, sol.status as stockOutLineStatus, sol.qty as stockOutLineQty, - spl.suggestedLotLineId as debugSuggestedLotLineId, + COALESCE(spl.suggestedLotLineId, ill.id) as debugSuggestedLotLineId, -- ✅ 使用COALESCE处理null值 ill.inventoryLotId as debugInventoryLotId, -- ✅ Calculate total picked quantity by ALL pick orders for this lot COALESCE(( @@ -831,43 +837,42 @@ open class PickOrderService( AND sol_all.deleted = false AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed') ), 0) as totalPickedByAllPickOrders, - -- ✅ Calculate remaining available quantity after all pick orders - (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0) - COALESCE(( - SELECT SUM(sol_all.qty) - FROM fpsmsdb.stock_out_line sol_all - WHERE sol_all.inventoryLotLineId = ill.id - AND sol_all.deleted = false - AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed') - ), 0)) as remainingAfterAllPickOrders, + -- ✅ FIXED: Calculate remaining available quantity correctly + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders, -- ✅ Add detailed debug fields for lotAvailability calculation ill.status as debug_ill_status, (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) as debug_is_expired, sol.status as debug_sol_status, - (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0) - COALESCE(( - SELECT SUM(sol_all.qty) - FROM fpsmsdb.stock_out_line sol_all - WHERE sol_all.inventoryLotLineId = ill.id - AND sol_all.deleted = false - AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed') - ), 0)) as debug_remaining_stock, - (spl.qty - COALESCE(sol.qty, 0)) as debug_required_after_picked, + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as debug_remaining_stock, + (COALESCE(spl.qty, 0) - COALESCE(sol.qty, 0)) as debug_required_after_picked, CASE - -- ✅ FIXED: Only check if lot is expired or rejected + -- ✅ FIXED: Check if lot is expired WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' + -- ✅ FIXED: Check if lot has rejected stock out line for this pick order WHEN sol.status = 'rejected' THEN 'rejected' - -- ✅ FIXED: For this pick order, if it's suggested, it should be available - -- The holdQty already reserves the stock for this pick order + -- ✅ FIXED: Check if lot is unavailable due to insufficient stock + WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock' + -- ✅ FIXED: Check if lot status is unavailable + WHEN ill.status = 'unavailable' THEN 'status_unavailable' + -- ✅ Default to available ELSE 'available' END as lotAvailability - FROM fpsmsdb.suggested_pick_lot spl - JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId + FROM fpsmsdb.inventory_lot_line ill JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId LEFT JOIN fpsmsdb.item_uom sales_iu ON sales_iu.itemId = il.itemId AND sales_iu.salesUnit = true AND sales_iu.deleted = false LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = sales_iu.uomId - LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = spl.pickOrderLineId AND sol.inventoryLotLineId = spl.suggestedLotLineId - WHERE spl.pickOrderLineId = :pickOrderLineId - ORDER BY il.expiryDate ASC, il.lotNo ASC + -- ✅ FIXED: Include both suggested lots AND lots with stock out lines for this pick order + LEFT JOIN fpsmsdb.suggested_pick_lot spl ON spl.suggestedLotLineId = ill.id AND spl.pickOrderLineId = :pickOrderLineId + LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = :pickOrderLineId AND sol.inventoryLotLineId = ill.id AND sol.deleted = false + -- ✅ FIXED: Only include lots that are either suggested OR have stock out lines for this pick order + WHERE (spl.pickOrderLineId = :pickOrderLineId OR sol.pickOrderLineId = :pickOrderLineId) + AND ill.deleted = false + AND il.deleted = false + ORDER BY + CASE WHEN sol.status = 'rejected' THEN 0 ELSE 1 END, -- ✅ Show rejected lots first + il.expiryDate ASC, + il.lotNo ASC """.trimIndent() println("🔍 Executing SQL for lot details: $sql") @@ -1070,23 +1075,106 @@ open class PickOrderService( @Throws(IOException::class) @Transactional open fun completeStockOut(consoCode: String): MessageResponse { + println("=== DEBUG: completeStockOut ===") + println("consoCode: $consoCode") + val stockOut = stockOutRepository.findByConsoPickOrderCode(consoCode).orElseThrow() - val stockOutLines = stockOutLIneRepository.findAllByStockOutId(stockOut.id!!) + println("StockOut ID: ${stockOut.id}") + + // ✅ FIXED: 直接通过 consoCode 查找相关的 stock out lines,而不依赖 stockOutId 关联 + val stockOutLinesSql = """ + SELECT sol.* + FROM stock_out_line sol + JOIN pick_order_line pol ON pol.id = sol.pickOrderLineId + JOIN pick_order po ON po.id = pol.poId + WHERE po.consoCode = :consoCode + AND sol.deleted = false + """.trimIndent() + + val stockOutLinesResult = jdbcDao.queryForList(stockOutLinesSql, mapOf("consoCode" to consoCode)) + println("=== DEBUG: Stock Out Lines for consoCode $consoCode ===") + stockOutLinesResult.forEach { row -> + println("StockOutLine ID: ${row["id"]}, stockOutId: ${row["stockOutId"]}, status: ${row["status"]}, qty: ${row["qty"]}, pickOrderLineId: ${row["pickOrderLineId"]}") + } + + // ✅ 将结果转换为 StockOutLine 对象 + val stockOutLineIds = stockOutLinesResult.mapNotNull { row -> + val id = row["id"] + println("Raw ID value: $id (type: ${id?.javaClass?.simpleName})") + val convertedId = when (id) { + is Number -> id.toLong() + is String -> id.toLongOrNull() + else -> null + } + println("Converted ID: $convertedId") + convertedId + } + + val stockOutLines = if (stockOutLineIds.isNotEmpty()) { + stockOutLIneRepository.findAllById(stockOutLineIds) + } else { + emptyList() + } + + println("Total stock out lines for consoCode $consoCode: ${stockOutLines.size}") + + stockOutLines.forEach { line -> + println("Stock out line ${line.id}: status=${line.status}, qty=${line.qty}") + } + val unfinishedLines = stockOutLines.filter { it.status != StockOutLineStatus.COMPLETE.status && it.status != StockOutLineStatus.REJECTED.status } + + println("Unfinished lines: ${unfinishedLines.size}") + if (unfinishedLines.isEmpty()) { stockOut.apply { this.status = StockOutStatus.COMPLETE.status } val savedStockOut = stockOutRepository.saveAndFlush(stockOut) - val pickOrderEntries = pickOrderRepository.findAllByConsoCode(consoCode).map { - it.apply { - it.status = PickOrderStatus.COMPLETED + + // ✅ NEW APPROACH: Update through relationship chain + // 1. Get all pick order lines from completed stock out lines + val completedPickOrderLineIds = stockOutLines.mapNotNull { it.pickOrderLine?.id } + println("Completed pick order line IDs: $completedPickOrderLineIds") + + // 2. Get all pick order lines and update their status + if (completedPickOrderLineIds.isNotEmpty()) { + val pickOrderLines = pickOrderLineRepository.findAllById(completedPickOrderLineIds) + pickOrderLines.forEach { line -> + line.status = PickOrderLineStatus.COMPLETED + println("Updated pick order line ${line.id} to COMPLETED") + } + pickOrderLineRepository.saveAll(pickOrderLines) + println("✅ Updated ${pickOrderLines.size} pick order lines to COMPLETED status") + + // 3. Get all unique pick order IDs from the lines + val pickOrderIds = pickOrderLines.mapNotNull { it.pickOrder?.id }.distinct() + println("Affected pick order IDs: $pickOrderIds") + + // 4. Check if each pick order is fully completed + pickOrderIds.forEach { pickOrderId -> + val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) + if (pickOrder != null) { + // Check if all lines of this pick order are completed + val allLines = pickOrder.pickOrderLines + val completedLines = allLines.filter { it.status == PickOrderLineStatus.COMPLETED } + + println("Pick order ${pickOrder.code}: ${completedLines.size}/${allLines.size} lines completed") + + if (completedLines.size == allLines.size && allLines.isNotEmpty()) { + // All lines are completed, update pick order status + pickOrder.status = PickOrderStatus.COMPLETED + pickOrder.completeDate = LocalDateTime.now() + pickOrderRepository.save(pickOrder) + println("✅ Updated pick order ${pickOrder.code} to COMPLETED status") + } + } } } - pickOrderRepository.saveAll(pickOrderEntries) + return MessageResponse( id = savedStockOut.id, name = savedStockOut.consoPickOrderCode ?: savedStockOut.deliveryOrderCode, @@ -1096,6 +1184,7 @@ open class PickOrderService( errorPosition = null, ) } else { + println("❌ Still have unfinished lines, cannot complete") return MessageResponse( id = stockOut.id, name = stockOut.consoPickOrderCode ?: stockOut.deliveryOrderCode, @@ -1107,6 +1196,83 @@ open class PickOrderService( } } + @Transactional(rollbackFor = [java.lang.Exception::class]) + open fun checkAndCompletePickOrderByConsoCode(consoCode: String): MessageResponse { + try { + println("=== DEBUG: checkAndCompletePickOrderByConsoCode ===") + println("consoCode: $consoCode") + + val stockOut = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null) + if (stockOut == null) { + println("❌ No stock_out found for consoCode: $consoCode") + return MessageResponse( + id = null, + name = "Stock out not found", + code = "ERROR", + type = "pickorder", + message = "No stock out found for consoCode: $consoCode", + errorPosition = null + ) + } + + val stockOutLinesSql = """ + SELECT sol.* + FROM stock_out_line sol + JOIN pick_order_line pol ON pol.id = sol.pickOrderLineId + JOIN pick_order po ON po.id = pol.poId + WHERE po.consoCode = :consoCode + AND sol.deleted = false + """.trimIndent() + + val stockOutLinesResult = jdbcDao.queryForList(stockOutLinesSql, mapOf("consoCode" to consoCode)) + val stockOutLineIds = stockOutLinesResult.mapNotNull { row -> + when (val id = row["id"]) { + is Number -> id.toLong() + is String -> id.toLongOrNull() + else -> null + } + } + val stockOutLines = if (stockOutLineIds.isNotEmpty()) { + stockOutLIneRepository.findAllById(stockOutLineIds) + } else { + emptyList() + } + val unfinishedLines = stockOutLines.filter { + it.status != StockOutLineStatus.COMPLETE.status + && it.status != StockOutLineStatus.REJECTED.status + } + + println("📊 Stock out lines: ${stockOutLines.size}, Unfinished: ${unfinishedLines.size}") + + if (unfinishedLines.isEmpty()) { + println("✅ All stock out lines completed, updating pick order statuses...") + return completeStockOut(consoCode) + } else { + println("⏳ Still have ${unfinishedLines.size} unfinished lines") + return MessageResponse( + id = stockOut.id, + name = stockOut.consoPickOrderCode ?: stockOut.deliveryOrderCode, + code = "NOT_COMPLETED", + type = stockOut.type, + message = "Pick order not completed yet, ${unfinishedLines.size} lines remaining", + errorPosition = null + ) + } + + } catch (e: Exception) { + println("❌ Error in checkAndCompletePickOrderByConsoCode: ${e.message}") + e.printStackTrace() + return MessageResponse( + id = null, + name = "Failed to check pick order completion", + code = "ERROR", + type = "pickorder", + message = "Failed to check pick order completion: ${e.message}", + errorPosition = null + ) + } + } + open fun createGroup(name: String, targetDate: LocalDate, pickOrderId: Long?): PickOrderGroup { val group = PickOrderGroup().apply { @@ -1135,6 +1301,7 @@ open class PickOrderService( open fun allPickOrdersGroup(): List { return pickOrderGroupRepository.findAllByDeletedIsFalse() } + open fun getLatestGroupNameAndCreate(): MessageResponse { // Use the working repository method like the old /latest endpoint val allGroups = allPickOrdersGroup() @@ -1183,6 +1350,7 @@ open class PickOrderService( "A001" // If no groups exist, return A001 } } + open fun createNewGroups(request: SavePickOrderGroupRequest): MessageResponse { val updatedGroups = mutableListOf() val createdGroups = mutableListOf() @@ -1362,13 +1530,17 @@ open class PickOrderService( } - open fun getAllPickOrderLotsWithDetails(userId: Long? = null): List> { + fun getAllPickOrderLotsWithDetails(pickOrderIds: List): List> { val today = LocalDate.now() val zero = BigDecimal.ZERO println("=== Debug: getAllPickOrderLotsWithDetails ===") println("today: $today") - println("userId filter: $userId") + println("pickOrderIds: $pickOrderIds") + + if (pickOrderIds.isEmpty()) { + return emptyList() + } val sql = """ SELECT @@ -1376,6 +1548,7 @@ open class PickOrderService( po.id as pickOrderId, po.code as pickOrderCode, po.targetDate as pickOrderTargetDate, + po.consoCode as pickOrderConsoCode, po.type as pickOrderType, po.status as pickOrderStatus, po.assignTo as pickOrderAssignTo, @@ -1406,70 +1579,519 @@ open class PickOrderService( ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) END as availableQty, - CASE - WHEN sol.status = 'rejected' THEN NULL - ELSE spl.qty - END as requiredQty, - - CASE - WHEN sol.status = 'rejected' THEN NULL - ELSE COALESCE(sol.qty, 0) - END as actualPickQty, - + -- Required quantity for this lot + COALESCE(spl.qty, 0) as requiredQty, + + -- Actual picked quantity + COALESCE(sol.qty, 0) as actualPickQty, + + -- Suggested pick lot information spl.id as suggestedPickLotId, - ill.status as lotStatus, + sol.status as lotStatus, + + -- Stock out line information sol.id as stockOutLineId, sol.status as stockOutLineStatus, - sol.qty as stockOutLineQty, + COALESCE(sol.qty, 0) as stockOutLineQty, - -- Calculated fields + -- Lot availability status CASE WHEN sol.status = 'rejected' THEN 'rejected' - WHEN ill.status != 'available' THEN 'unavailable' - WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' - WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) < (spl.qty) THEN 'insufficient_stock' + WHEN sol.status = 'completed' THEN 'completed' + WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'unavailable' ELSE 'available' END as lotAvailability, - -- Stock out line status for filtering + -- Processing status CASE - WHEN sol.status = 'completed' AND sol.qty = spl.qty THEN 'completed' - WHEN sol.status = 'completed' AND sol.qty < spl.qty THEN 'partially_completed' + WHEN sol.status = 'completed' THEN 'completed' WHEN sol.status = 'rejected' THEN 'rejected' - WHEN sol.status = 'pending' THEN 'pending' - WHEN sol.status = 'checked' THEN 'checked' - ELSE 'not_started' + WHEN sol.status = 'created' THEN 'pending' + ELSE 'pending' END as processingStatus + FROM pick_order po + LEFT JOIN pick_order_group pog ON po.groupId = pog.id + LEFT JOIN pick_order_line pol ON po.id = pol.pickOrderId + LEFT JOIN items i ON pol.itemId = i.id + LEFT JOIN uom_conversion uc ON i.uomId = uc.id + LEFT JOIN suggested_pick_lot spl ON pol.id = spl.pickOrderLineId + LEFT JOIN inventory_lot_line ill ON spl.suggestedLotLineId = ill.id + LEFT JOIN inventory_lot il ON ill.lotId = il.id + LEFT JOIN warehouse w ON ill.warehouseId = w.id + LEFT JOIN stock_out_line sol ON spl.id = sol.suggestedPickLotId + WHERE po.id IN (${pickOrderIds.joinToString(",")}) + AND po.status IN ('assigned', 'released', 'picking') + AND pol.status IN ('assigned', 'released', 'picking') + AND (sol.status IS NULL OR sol.status != 'completed') + ORDER BY po.id, pol.id, ill.id + """.trimIndent() + + println("🔍 Executing SQL for all pick order lots: $sql") + + val result = jdbcDao.queryForList(sql, emptyMap()) + + println("Total result count (including completed and rejected): ${result.size}") + result.forEach { row -> + println("Row: $row") + } + + return result + } + + open fun getPickOrderDetailsOptimized(pickOrderIds: List): GetPickOrderInfoResponse { + val today = LocalDate.now() + val zero = BigDecimal.ZERO + + if (pickOrderIds.isEmpty()) { + return GetPickOrderInfoResponse( + consoCode = null, + pickOrders = emptyList(), + items = emptyList() + ) + } + + val pickOrderIdsStr = pickOrderIds.joinToString(",") + + val sql = """ + SELECT + -- Pick Order Information + po.id as pickOrderId, + po.code as pickOrderCode, + po.consoCode as pickOrderConsoCode, -- ✅ 添加 consoCode + po.targetDate as pickOrderTargetDate, + po.type as pickOrderType, + po.status as pickOrderStatus, + po.assignTo as pickOrderAssignTo, + + -- Pick Order Line Information + pol.id as pickOrderLineId, + pol.qty as pickOrderLineRequiredQty, + + -- Item Information + i.id as itemId, + i.code as itemCode, + i.name as itemName, + uc.code as uomCode, + uc.udfudesc as uomDesc, + + -- ✅ Calculate total picked quantity from stock_out_line table + COALESCE(( + SELECT SUM(sol_picked.qty) + FROM fpsmsdb.stock_out_line sol_picked + WHERE sol_picked.pickOrderLineId = pol.id + AND sol_picked.deleted = false + AND sol_picked.status IN ('completed', 'COMPLETE', 'partially_completed','rejected') + ), 0) as totalPickedQty, + + -- ✅ Calculate available quantity from inventory + COALESCE(( + SELECT inv.onHandQty - inv.onHoldQty - inv.unavailableQty + FROM fpsmsdb.inventory inv + WHERE inv.itemId = i.id + AND inv.deleted = false + ), 0) as availableQty + FROM fpsmsdb.pick_order po JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id JOIN fpsmsdb.items i ON i.id = pol.itemId LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId - LEFT JOIN fpsmsdb.pick_order_group pog ON pog.pick_order_id = po.id AND pog.deleted = false - JOIN fpsmsdb.suggested_pick_lot spl ON spl.pickOrderLineId = pol.id - JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId - JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId - LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId - LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ill.id WHERE po.deleted = false - AND po.status = 'RELEASED' + AND po.id IN ($pickOrderIdsStr) AND pol.deleted = false - AND ill.deleted = false - AND il.deleted = false - ${if (userId != null) "AND po.assignTo = :userId" else ""} + AND po.status = 'RELEASED' + + ORDER BY + po.code ASC, + i.code ASC + """.trimIndent() + + println("🔍 Executing optimized SQL: $sql") + + val result = jdbcDao.queryForList(sql, emptyMap()) + + println("✅ Optimized result count: ${result.size}") + + // ✅ Transform the flat data into the expected nested structure + val pickOrdersMap = mutableMapOf() + var consoCode: String? = null // ✅ 用于存储 consoCode + + result.forEach { row -> + val pickOrderId = (row["pickOrderId"] as Number).toLong() + val pickOrderLineId = (row["pickOrderLineId"] as Number).toLong() + + // ✅ 获取 consoCode(所有行应该有相同的 consoCode) + if (consoCode == null) { + consoCode = row["pickOrderConsoCode"] as String? + } + + // Create or get pick order + if (!pickOrdersMap.containsKey(pickOrderId)) { + pickOrdersMap[pickOrderId] = GetPickOrderInfo( + id = pickOrderId, + code = row["pickOrderCode"] as String, + consoCode = row["pickOrderConsoCode"] as String?, + targetDate = row["pickOrderTargetDate"] as LocalDateTime?, // ✅ Convert to LocalDateTime + type = row["pickOrderType"] as String, + status = row["pickOrderStatus"] as String, + assignTo = (row["pickOrderAssignTo"] as Number?)?.toLong(), + groupName = "No Group", + pickOrderLines = mutableListOf() + ) + } + + val pickOrder = pickOrdersMap[pickOrderId]!! - UNION ALL + // Create pick order line + val pickOrderLine = GetPickOrderLineInfo( + id = pickOrderLineId, + itemId = (row["itemId"] as Number).toLong(), + itemCode = row["itemCode"] as String, + itemName = row["itemName"] as String, + availableQty = (row["availableQty"] as Number).toDouble().toBigDecimal(), // ✅ Convert to BigDecimal + requiredQty = (row["pickOrderLineRequiredQty"] as Number).toDouble() + .toBigDecimal(), // ✅ Convert to BigDecimal + uomCode = row["uomCode"] as String?, + uomDesc = row["uomDesc"] as String?, + suggestedList = emptyList(), + pickedQty = (row["totalPickedQty"] as Number).toDouble().toBigDecimal() // ✅ Convert to BigDecimal + ) - -- ✅ ADD: Query for rejected lots that might not have suggested_pick_lot entries + // ✅ Fix the add method call + (pickOrder.pickOrderLines as MutableList).add(pickOrderLine) + } + + return GetPickOrderInfoResponse( + consoCode = consoCode, // ✅ 使用获取到的 consoCode + pickOrders = pickOrdersMap.values.toList(), + items = emptyList() + ) + } + + open fun getPickOrderDetailsOptimizedByUser(userId: Long): GetPickOrderInfoResponse { + val today = LocalDate.now() + val zero = BigDecimal.ZERO + + // Get all released pick order IDs for the user + val allPickOrders = pickOrderRepository.findAll() + val releasedPickOrderIds = allPickOrders + .filter { it.status == PickOrderStatus.RELEASED } + .filter { it.assignTo?.id == userId } + .map { it.id!! } + + if (releasedPickOrderIds.isEmpty()) { + return GetPickOrderInfoResponse( + consoCode = null, + pickOrders = emptyList(), + items = emptyList() + ) + } + + // Use the existing optimized method + return getPickOrderDetailsOptimized(releasedPickOrderIds) + } + + +// ... existing code ... + + @Transactional(rollbackFor = [java.lang.Exception::class]) + open fun autoAssignAndReleasePickOrder(userId: Long): MessageResponse { + try { + println("=== DEBUG: autoAssignAndReleasePickOrder ===") + println("userId: $userId") + + val zero = BigDecimal.ZERO + val releasedBy = SecurityUtils.getUser().getOrNull() + val user = userService.find(userId).orElse(null) + + if (user == null) { + println("❌ User not found: $userId") + return MessageResponse( + id = null, + name = "User not found", + code = "ERROR", + type = "pickorder", + message = "User with ID $userId not found", + errorPosition = null + ) + } + + println("✅ User found: ${user.name} (ID: ${user.id})") + + // Check if user already has pending/released pick orders that are not completed + val existingPickOrders = pickOrderRepository.findAllByAssignToAndStatusIn( + user, + listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED) + ) + + println("📋 Found ${existingPickOrders.size} existing pick orders for user") + + // Filter out completed pick orders by checking stock_out status + val activePickOrders = existingPickOrders.filter { pickOrder -> + // ✅ Fix: Store consoCode in local variable to avoid smart cast issues + val consoCode = pickOrder.consoCode + if (consoCode == null) { + println("🔍 Checking pick order ${pickOrder.code} (consoCode: null)") + println(" ⚠️ No consoCode - considering as active") + true // No consoCode means not completed + } else { + val stockOut = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null) + println("�� Checking pick order ${pickOrder.code} (consoCode: $consoCode)") + + if (stockOut == null) { + println(" ⚠️ No stock_out record found - considering as active") + true // No stock_out record means not completed + } else { + println(" 📦 Stock_out status: ${stockOut.status}") + val isActive = stockOut.status != StockOutStatus.COMPLETE.status + println(" ${if (isActive) "🟡 Active" else "✅ Completed"}") + isActive + } + } + } + + println("🎯 Active pick orders: ${activePickOrders.size}") + + if (activePickOrders.isNotEmpty()) { + // User already has active pick orders, return existing ones + println("ℹ️ User already has active pick orders") + return MessageResponse( + id = null, + name = "User already has active pick orders", + code = "EXISTS", + type = "pickorder", + message = "User already has ${activePickOrders.size} active pick order(s)", + errorPosition = null, + entity = mapOf( + "pickOrderIds" to activePickOrders.map { it.id!! }, + "hasActiveOrders" to true + ) + ) + } + + // Find available pick orders (prioritize those with doId) + val availablePickOrders = pickOrderRepository.findAllByStatusAndDeletedFalse(PickOrderStatus.PENDING) + .filter { it.deliveryOrder != null } // Only pick orders with doId (delivery order) + .sortedBy { it.targetDate } // Sort by target date (earliest first) + .take(1) // Take only one pick order for now + + println("📋 Available pick orders with doId: ${availablePickOrders.size}") + + if (availablePickOrders.isEmpty()) { + println("❌ No available pick orders with doId found") + return MessageResponse( + id = null, + name = "No available pick orders", + code = "NO_ORDERS", + type = "pickorder", + message = "No pending pick orders with delivery orders available for assignment", + errorPosition = null, + entity = mapOf("hasActiveOrders" to false) + ) + } + + println("✅ Found available pick order: ${availablePickOrders.first().code}") + + // ✅ Add the complete assignment logic here + val availablePickOrder = availablePickOrders.first() + + // Generate consoCode early and save immediately + val newConsoCode = assignConsoCode() + val currUser = SecurityUtils.getUser().orElseThrow() + + // Create and save StockOut immediately to prevent duplicate consoCodes + val stockOut = StockOut().apply { + this.type = "job" + this.consoPickOrderCode = newConsoCode + this.status = StockOutStatus.PENDING.status + this.handler = currUser.id + } + val savedStockOut = stockOutRepository.saveAndFlush(stockOut) + + // Assign and release the pick order + availablePickOrder.apply { + this.releasedBy = releasedBy + status = PickOrderStatus.RELEASED + this.assignTo = user + this.consoCode = newConsoCode + } + + val suggestions = suggestedPickLotService.suggestionForPickOrders( + SuggestedPickLotForPoRequest(pickOrders = listOf(availablePickOrder)) + ) + + val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) + + pickOrderRepository.saveAndFlush(availablePickOrder) + + val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( + saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } + ) + + saveSuggestedPickLots.forEach { lot -> + if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) { + val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine) + if (lineIndex >= 0) { + inventoryLotLines[lineIndex].holdQty = + (inventoryLotLines[lineIndex].holdQty ?: zero).plus(lot.qty ?: zero) + } + } + } + + inventoryLotLineRepository.saveAll(inventoryLotLines) + + println("✅ Successfully assigned pick order ${availablePickOrder.code}") + + return MessageResponse( + id = null, + name = "Pick order automatically assigned and released", + code = "SUCCESS", + type = "pickorder", + message = "Pick order ${availablePickOrder.code} assigned and released to user ${user.name}", + errorPosition = null, + entity = mapOf( + "consoCode" to newConsoCode, + "pickOrderIds" to listOf(availablePickOrder.id!!), + "hasActiveOrders" to false + ) + ) + + } catch (e: Exception) { + println("❌ Error in autoAssignAndReleasePickOrder: ${e.message}") + e.printStackTrace() + return MessageResponse( + id = null, + name = "Failed to auto-assign pick order", + code = "ERROR", + type = "pickorder", + message = "Failed to auto-assign pick order: ${e.message}", + errorPosition = null + ) + } + } + + open fun checkPickOrderCompletion(userId: Long): MessageResponse { + try { + val user = userService.find(userId).orElse(null) + + if (user == null) { + return MessageResponse( + id = null, + name = "User not found", + code = "ERROR", + type = "pickorder", + message = "User with ID $userId not found", + errorPosition = null + ) + } + + // Get all pick orders assigned to this user + val userPickOrders = pickOrderRepository.findAllByAssignToAndStatusIn( + user, + listOf(PickOrderStatus.ASSIGNED, PickOrderStatus.RELEASED) + ) + + val completionStatus = userPickOrders.map { pickOrder -> + // ✅ Fix: Store consoCode in local variable to avoid smart cast issues + val consoCode = pickOrder.consoCode + val stockOut = if (consoCode != null) { + stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null) + } else { + null + } + + val stockOutLines = if (stockOut != null) { + stockOutLIneRepository.findAllByStockOutId(stockOut.id!!) + } else { + emptyList() + } + + val unfinishedLines = stockOutLines.filter { + it.status != StockOutLineStatus.COMPLETE.status && it.status != StockOutLineStatus.REJECTED.status + } + + mapOf( + "pickOrderId" to pickOrder.id, + "pickOrderCode" to pickOrder.code, + "consoCode" to consoCode, + "isCompleted" to (unfinishedLines.isEmpty() && stockOut != null), + "stockOutStatus" to (stockOut?.status ?: "not_created"), + "totalLines" to stockOutLines.size, + "unfinishedLines" to unfinishedLines.size + ) + } + + val completedOrders = completionStatus.filter { it["isCompleted"] == true } + val hasCompletedOrders = completedOrders.isNotEmpty() + + return MessageResponse( + id = null, + name = "Pick order completion status checked", + code = "SUCCESS", + type = "pickorder", + message = "Found ${completedOrders.size} completed orders out of ${userPickOrders.size} total", + errorPosition = null, + entity = mapOf( + "hasCompletedOrders" to hasCompletedOrders, + "completedOrders" to completedOrders, + "allOrders" to completionStatus + ) + ) + + } catch (e: Exception) { + return MessageResponse( + id = null, + name = "Failed to check pick order completion", + code = "ERROR", + type = "pickorder", + message = "Failed to check pick order completion: ${e.message}", + errorPosition = null + ) + } + } + open fun getAllPickOrderLotsWithDetailsWithAutoAssign(userId: Long): List> { + println("=== Debug: getAllPickOrderLotsWithDetailsWithAutoAssign ===") + println("today: ${LocalDate.now()}") + println("userId filter: $userId") + + // ✅ First attempt auto-assignment for the user + println("🎯 Attempting auto-assignment for user $userId") + val assignedPickOrderResponse = autoAssignAndReleasePickOrder(userId) + + // Get all pick order IDs assigned to the user (both RELEASED and PENDING with doId) + val user = userService.find(userId).orElse(null) + if (user == null) { + println("❌ User not found: $userId") + return emptyList() + } + + // Get all pick orders assigned to user with PENDING or RELEASED status that have doId + val allAssignedPickOrders = pickOrderRepository.findAllByAssignToAndStatusIn( + user, + listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED) + ).filter { it.deliveryOrder != null } // Only pick orders with doId + + val pickOrderIds = allAssignedPickOrders.map { it.id!! } + + println(" Pick order IDs to fetch: $pickOrderIds") + + if (pickOrderIds.isEmpty()) { + return emptyList() + } + + // ✅ Use SQL query approach similar to getPickOrderLineLotDetails for detailed lot information + val pickOrderIdsStr = pickOrderIds.joinToString(",") + + val sql = """ SELECT -- Pick Order Information po.id as pickOrderId, po.code as pickOrderCode, - po.targetDate as pickOrderTargetDate, + po.consoCode as pickOrderConsoCode, + DATE_FORMAT(po.targetDate, '%Y-%m-%d') as pickOrderTargetDate, po.type as pickOrderType, po.status as pickOrderStatus, po.assignTo as pickOrderAssignTo, - pog.name as groupName, -- Pick Order Line Information pol.id as pickOrderLineId, @@ -1483,67 +2105,121 @@ open class PickOrderService( uc.code as uomCode, uc.udfudesc as uomDesc, - -- Lot Information + -- Lot Information (similar to lot-details endpoint) ill.id as lotId, il.lotNo, - il.expiryDate, + DATE_FORMAT(il.expiryDate, '%Y-%m-%d') as expiryDate, w.name as location, COALESCE(uc.udfudesc, 'N/A') as stockUnit, - -- ✅ NULL for rejected lots - NULL as availableQty, - NULL as requiredQty, - NULL as actualPickQty, + -- ✅ FIXED: Set quantities to NULL for rejected lots + CASE + WHEN sol.status = 'rejected' THEN NULL + ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) + END as availableQty, + + -- Required quantity for this lot + COALESCE(spl.qty, 0) as requiredQty, + + -- Actual picked quantity + COALESCE(sol.qty, 0) as actualPickQty, - NULL as suggestedPickLotId, + -- Suggested pick lot information + spl.id as suggestedPickLotId, ill.status as lotStatus, + + -- Stock out line information sol.id as stockOutLineId, sol.status as stockOutLineStatus, - sol.qty as stockOutLineQty, + COALESCE(sol.qty, 0) as stockOutLineQty, + + -- Additional detailed fields from lot-details + COALESCE(ill.inQty, 0) as inQty, + COALESCE(ill.outQty, 0) as outQty, + COALESCE(ill.holdQty, 0) as holdQty, + COALESCE(spl.suggestedLotLineId, ill.id) as debugSuggestedLotLineId, + ill.inventoryLotId as debugInventoryLotId, + + -- Calculate total picked quantity by ALL pick orders for this lot + COALESCE(( + SELECT SUM(sol_all.qty) + FROM fpsmsdb.stock_out_line sol_all + WHERE sol_all.inventoryLotLineId = ill.id + AND sol_all.deleted = false + AND sol_all.status IN ('pending', 'checked', 'partially_completed', 'completed') + ), 0) as totalPickedByAllPickOrders, - -- Calculated fields - 'rejected' as lotAvailability, - 'rejected' as processingStatus + -- Calculate remaining available quantity correctly + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as remainingAfterAllPickOrders, + + -- Add detailed debug fields for lotAvailability calculation + ill.status as debug_ill_status, + (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) as debug_is_expired, + sol.status as debug_sol_status, + (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as debug_remaining_stock, + (COALESCE(spl.qty, 0) - COALESCE(sol.qty, 0)) as debug_required_after_picked, + + -- Lot availability status (same logic as lot-details) + CASE + -- Check if lot is expired + WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' + -- Check if lot has rejected stock out line for this pick order + WHEN sol.status = 'rejected' THEN 'rejected' + -- Check if lot is unavailable due to insufficient stock + WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) <= 0 THEN 'insufficient_stock' + -- Check if lot status is unavailable + WHEN ill.status = 'unavailable' THEN 'status_unavailable' + -- Default to available + ELSE 'available' + END as lotAvailability, + + -- Processing status + CASE + WHEN sol.status = 'completed' THEN 'completed' + WHEN sol.status = 'rejected' THEN 'rejected' + WHEN sol.status = 'created' THEN 'pending' + ELSE 'pending' + END as processingStatus FROM fpsmsdb.pick_order po JOIN fpsmsdb.pick_order_line pol ON pol.poId = po.id JOIN fpsmsdb.items i ON i.id = pol.itemId LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = pol.uomId - LEFT JOIN fpsmsdb.pick_order_group pog ON pog.pick_order_id = po.id AND pog.deleted = false - JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id - JOIN fpsmsdb.inventory_lot_line ill ON ill.id = sol.inventoryLotLineId - JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId + LEFT JOIN fpsmsdb.suggested_pick_lot spl ON pol.id = spl.pickOrderLineId + LEFT JOIN fpsmsdb.inventory_lot_line ill ON spl.suggestedLotLineId = ill.id + LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId + LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.inventoryLotLineId = ill.id AND sol.deleted = false WHERE po.deleted = false - AND po.status = 'RELEASED' + AND po.id IN ($pickOrderIdsStr) AND pol.deleted = false + AND po.status IN ('PENDING', 'RELEASED') + AND po.assignTo = :userId AND ill.deleted = false AND il.deleted = false - AND sol.status = 'rejected' - ${if (userId != null) "AND po.assignTo = :userId" else ""} - + AND (spl.pickOrderLineId IS NOT NULL OR sol.pickOrderLineId IS NOT NULL) ORDER BY - pickOrderCode ASC, - itemCode ASC, - expiryDate ASC, - lotNo ASC + CASE WHEN sol.status = 'rejected' THEN 0 ELSE 1 END, -- Show rejected lots first + po.code ASC, + i.code ASC, + il.expiryDate ASC, + il.lotNo ASC """.trimIndent() - - println("🔍 Executing SQL for all pick order lots: $sql") - val params = if (userId != null) { - mapOf("userId" to userId) - } else { - emptyMap() - } - - val result = jdbcDao.queryForList(sql, params) + println("🔍 Executing SQL for all pick order lots with details: $sql") + println(" With parameters: userId = $userId, pickOrderIds = $pickOrderIdsStr") - println("Total result count (including completed and rejected): ${result.size}") - result.forEach { row -> - println("Row: $row") + val results = jdbcDao.queryForList(sql, mapOf("userId" to userId)) + println("✅ Total result count: ${results.size}") + + // Filter out lots with null availableQty (rejected lots) + val filteredResults = results.filter { row -> + val availableQty = row["availableQty"] + availableQty != null } - - return result + + println("✅ Filtered result count: ${filteredResults.size}") + + return filteredResults } -} \ No newline at end of file +} diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt new file mode 100644 index 0000000..0f5b228 --- /dev/null +++ b/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 { + return pickExecutionIssueService.getPickExecutionIssuesByPickOrder(pickOrderId) + } + + @GetMapping("/issues/pickOrderLine/{pickOrderLineId}") + fun getPickExecutionIssuesByPickOrderLine(@PathVariable pickOrderLineId: Long): List { + return pickExecutionIssueService.getPickExecutionIssuesByPickOrderLine(pickOrderLineId) + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt index ebfae81..c81a3e4 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt +++ b/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> { - 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> { + return pickOrderService.getAllPickOrderLotsWithDetailsWithAutoAssign(userId) + } + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt index dc50156..0a63569 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/ConsoPickOrderResponse.kt +++ b/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?, diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/PickExecutionIssueRequest.kt new file mode 100644 index 0000000..24dc673 --- /dev/null +++ b/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 +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/FailInventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/FailInventoryLotLineRepository.kt new file mode 100644 index 0000000..ceea046 --- /dev/null +++ b/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 { + fun findByStockOutLineIdAndDeletedFalse(stockOutLineId: Int): FailInventoryLotLine? +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/Failinventorylotline.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/Failinventorylotline.kt new file mode 100644 index 0000000..f06d677 --- /dev/null +++ b/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() { + + @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 +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index 1d0599b..689988e 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/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 { @@ -34,4 +35,6 @@ interface InventoryLotLineRepository : AbstractRepository fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: String): List + fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: InventoryLotLineStatus): List + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt index aab25dd..7190ee1 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt +++ b/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) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 4ca62dd..7e06246 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/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") diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 6b7397b..3cb69a5 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/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 = mutableListOf() val holdQtyMap: MutableMap = 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() - + 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() 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() + + // 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): List { val allPickOrderLines = mutableListOf() @@ -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 ... + } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt index 9fc3e2f..40d9d58 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/SuggestedPickLotController.kt +++ b/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) +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/PickAnotherLotRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/PickAnotherLotRequest.kt new file mode 100644 index 0000000..7a870f5 --- /dev/null +++ b/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 // 可选:记录日期,默认为当前日期 +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/UpdateSuggestedLotLineIdRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/UpdateSuggestedLotLineIdRequest.kt new file mode 100644 index 0000000..1efe895 --- /dev/null +++ b/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 +) \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20250902_02_enson/01_create_group_enson.sql b/src/main/resources/db/changelog/changes/20250902_02_enson/01_create_group_enson.sql new file mode 100644 index 0000000..b18d408 --- /dev/null +++ b/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 \ No newline at end of file