# Conflicts: # src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.ktmaster
| @@ -462,18 +462,17 @@ open class M18DeliveryOrderService( | |||
| } | |||
| // End of save. Check result | |||
| // logger.info("Total Success (${doRefType}) (${successList.size}): $successList") | |||
| logger.info("Total Success (${doRefType}) (${successList.size})") | |||
| // if (failList.size > 0) { | |||
| logger.error("Total Fail (${doRefType}) (${failList.size}): $failList") | |||
| // } | |||
| // logger.info("Total Success (${doLineRefType}) (${successDetailList.size}): $successDetailList") | |||
| logger.info("Total Success (${doLineRefType}) (${successDetailList.size})") | |||
| // if (failDetailList.size > 0) { | |||
| logger.error("Total Fail (${doLineRefType}) (${failDetailList.size}): $failDetailList") | |||
| // logger.error("Total Fail M18 Items (${doLineRefType}) (${failItemDetailList.distinct().size}): ${failItemDetailList.distinct()}") | |||
| // } | |||
| val feeMarked = deliveryOrderLineService.markDeletedLinesWithFeeItems() | |||
| if (feeMarked > 0) { | |||
| logger.info("Marked $feeMarked DO line(s) as deleted (isFee items).") | |||
| } | |||
| logger.info("--------------------------------------------End - Saving M18 Delivery Order--------------------------------------------") | |||
| return SyncResult( | |||
| @@ -1,10 +1,17 @@ | |||
| package com.ffii.fpsms.modules.deliveryOrder.entity | |||
| import com.ffii.core.support.AbstractRepository | |||
| import org.springframework.data.jpa.repository.Query | |||
| import org.springframework.stereotype.Repository | |||
| import java.io.Serializable | |||
| @Repository | |||
| interface DeliveryOrderLineRepository : AbstractRepository<DeliveryOrderLine, Long> { | |||
| fun findByM18DataLogIdAndDeletedIsFalse(m18datalogId: Serializable): DeliveryOrderLine? | |||
| @Query( | |||
| "SELECT dol FROM DeliveryOrderLine dol " + | |||
| "WHERE dol.deleted = false AND dol.item IS NOT NULL AND dol.item.isFee = true" | |||
| ) | |||
| fun findAllByDeletedIsFalseAndItemIsFeeTrue(): List<DeliveryOrderLine> | |||
| } | |||
| @@ -68,4 +68,13 @@ open class DeliveryOrderLineService( | |||
| return savedDeliveryOrderLine | |||
| } | |||
| open fun markDeletedLinesWithFeeItems(): Int { | |||
| val feeLines = deliveryOrderLineRepository.findAllByDeletedIsFalseAndItemIsFeeTrue() | |||
| feeLines.forEach { line -> | |||
| line.deleted = true | |||
| deliveryOrderLineRepository.saveAndFlush(line) | |||
| } | |||
| return feeLines.size | |||
| } | |||
| } | |||
| @@ -45,6 +45,7 @@ import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoResponse | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoResponse | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsResponse | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.StockOutLineDetailResponse | |||
| import com.ffii.fpsms.modules.stock.entity.enum.StockInLineStatus | |||
| import com.ffii.fpsms.modules.stock.entity.StockInLineRepository | |||
| @@ -1960,6 +1961,23 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||
| val stockOutLinesByPickOrderLine = pickOrderLineIds.associateWith { polId -> | |||
| stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId) | |||
| } | |||
| // stockouts 可能包含不在 suggestedPickLots 內的 inventoryLotLineId,需補齊以便計算 location/availableQty | |||
| val stockOutInventoryLotLineIds = stockOutLinesByPickOrderLine.values | |||
| .flatten() | |||
| .mapNotNull { it.inventoryLotLineId } | |||
| .distinct() | |||
| val stockOutInventoryLotLines = if (stockOutInventoryLotLineIds.isNotEmpty()) { | |||
| inventoryLotLineRepository.findAllByIdIn(stockOutInventoryLotLineIds) | |||
| .filter { it.deleted == false } | |||
| } else { | |||
| emptyList() | |||
| } | |||
| val inventoryLotLineById = (inventoryLotLines + stockOutInventoryLotLines) | |||
| .filter { it.id != null } | |||
| .associateBy { it.id!! } | |||
| // 获取 stock in lines 通过 inventoryLotLineId(用于填充 stockInLineId) | |||
| val stockInLinesByInventoryLotLineId = if (inventoryLotLineIds.isNotEmpty()) { | |||
| @@ -2080,6 +2098,32 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||
| matchQty = jpo?.matchQty?.toDouble() | |||
| ) | |||
| } | |||
| // 构建 stockouts 数据:用于无 suggested lot / noLot 场景也能显示并闭环(submit 0) | |||
| val stockouts = (stockOutLinesByPickOrderLine[lineId] ?: emptyList()).map { sol -> | |||
| val illId = sol.inventoryLotLineId | |||
| val ill = if (illId != null) inventoryLotLineById[illId] else null | |||
| val lot = ill?.inventoryLot | |||
| val warehouse = ill?.warehouse | |||
| val availableQty = if (sol.status == "rejected") { | |||
| null | |||
| } else if (ill == null || ill.deleted == true) { | |||
| null | |||
| } else { | |||
| (ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO) | |||
| } | |||
| StockOutLineDetailResponse( | |||
| id = sol.id, | |||
| status = sol.status, | |||
| qty = sol.qty.toDouble(), | |||
| lotId = illId, | |||
| lotNo = sol.lotNo ?: lot?.lotNo, | |||
| location = warehouse?.code, | |||
| availableQty = availableQty?.toDouble(), | |||
| noLot = (illId == null) | |||
| ) | |||
| } | |||
| PickOrderLineWithLotsResponse( | |||
| id = pol.id!!, | |||
| @@ -2091,6 +2135,7 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||
| uomDesc = uom?.udfudesc, | |||
| status = pol.status?.value, | |||
| lots = lots, | |||
| stockouts = stockouts, | |||
| handler=handlerName | |||
| ) | |||
| } | |||
| @@ -96,9 +96,25 @@ data class PickOrderLineWithLotsResponse( | |||
| val uomDesc: String?, | |||
| val status: String?, | |||
| val lots: List<LotDetailResponse>, | |||
| val stockouts: List<StockOutLineDetailResponse> = emptyList(), | |||
| val handler: String? | |||
| ) | |||
| /** | |||
| * Stock-out line rows that should be shown even when there is no suggested lot. | |||
| * `noLot=true` indicates this line currently has no lot assigned / insufficient inventory lot. | |||
| */ | |||
| data class StockOutLineDetailResponse( | |||
| val id: Long?, | |||
| val status: String?, | |||
| val qty: Double?, | |||
| val lotId: Long?, | |||
| val lotNo: String?, | |||
| val location: String?, | |||
| val availableQty: Double?, | |||
| val noLot: Boolean | |||
| ) | |||
| data class LotDetailResponse( | |||
| val lotId: Long?, | |||
| val lotNo: String?, | |||
| @@ -92,10 +92,18 @@ open class PickExecutionIssueService( | |||
| println(" issueCategory: ${request.issueCategory}") | |||
| println("========================================") | |||
| // 1. 检查是否已经存在相同的 pick execution issue 记录 | |||
| // 1. 解析 lot: | |||
| // request.lotId 在前端目前传的是 inventory_lot_line.id(用于 SOL 关联/计算 bookQty 等) | |||
| // 但 pick_execution_issue.lot_id 在 DB 上外键指向 inventory_lot.id | |||
| val inventoryLotLine = request.lotId?.let { | |||
| inventoryLotLineRepository.findById(it).orElse(null) | |||
| } | |||
| val inventoryLotIdForIssue = inventoryLotLine?.inventoryLot?.id | |||
| // 2. 检查是否已经存在相同的 pick execution issue 记录(以 inventory_lot.id 去重) | |||
| val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( | |||
| request.pickOrderLineId, | |||
| request.lotId ?: 0L | |||
| request.pickOrderLineId, | |||
| inventoryLotIdForIssue ?: 0L | |||
| ) | |||
| println("Checking for existing issues...") | |||
| @@ -119,12 +127,8 @@ open class PickExecutionIssueService( | |||
| val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) | |||
| println("Pick order: id=${pickOrder?.id}, code=${pickOrder?.code}, type=${pickOrder?.type?.value}") | |||
| // 2. 获取 inventory_lot_line 并计算账面数量 (bookQty) | |||
| val inventoryLotLine = request.lotId?.let { | |||
| inventoryLotLineRepository.findById(it).orElse(null) | |||
| } | |||
| println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}") | |||
| // 3. 计算账面数量 (bookQty)(用 inventory_lot_line 快照) | |||
| println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}, inventoryLotId=${inventoryLotIdForIssue}") | |||
| // 计算账面数量(创建 issue 时的快照) | |||
| val bookQty = if (inventoryLotLine != null) { | |||
| @@ -138,13 +142,47 @@ open class PickExecutionIssueService( | |||
| BigDecimal.ZERO | |||
| } | |||
| // 3. 获取数量值 | |||
| // 4. 获取数量值 | |||
| val requiredQty = request.requiredQty ?: BigDecimal.ZERO | |||
| val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO | |||
| val missQty = request.missQty ?: BigDecimal.ZERO | |||
| val badItemQty = request.badItemQty ?: BigDecimal.ZERO | |||
| val badReason = request.badReason ?: "quantity_problem" | |||
| val relatedStockOutLines = stockOutLineRepository | |||
| .findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||
| request.pickOrderLineId, | |||
| request.lotId ?: 0L | |||
| ) | |||
| val currentStatus = relatedStockOutLines.firstOrNull()?.status ?: "" | |||
| if (currentStatus.equals("pending", ignoreCase = true) | |||
| && actualPickQty > BigDecimal.ZERO | |||
| && missQty == BigDecimal.ZERO | |||
| && badItemQty == BigDecimal.ZERO | |||
| ) { | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "Invalid issue for pending stock out line", | |||
| code = "ERROR", | |||
| type = "pick_execution_issue", | |||
| message = "Cannot submit only actual pick qty when stock out line is pending. Please rescan the lot or use normal pick flow.", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| val lotRemainAvailable = bookQty // 当前 lot 剩余 | |||
| val maxAllowed = requiredQty + lotRemainAvailable | |||
| if (actualPickQty > maxAllowed) { | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "Actual pick qty too large", | |||
| code = "ERROR", | |||
| type = "pick_execution_issue", | |||
| message = "Actual pick qty cannot exceed required qty plus lot remaining available.", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| println("=== Quantity Summary ===") | |||
| println(" Required Qty: $requiredQty") | |||
| println(" Actual Pick Qty: $actualPickQty") | |||
| @@ -153,7 +191,7 @@ open class PickExecutionIssueService( | |||
| println(" Bad Reason: $badReason") | |||
| println(" Book Qty: $bookQty") | |||
| // 4. 计算 issueQty(实际的问题数量) | |||
| // 5. 计算 issueQty(实际的问题数量) | |||
| val issueQty = when { | |||
| // Bad item 或 bad package:一律用用户输入的 bad 数量,不用 bookQty - actualPickQty | |||
| badItemQty > BigDecimal.ZERO -> { | |||
| @@ -179,10 +217,12 @@ open class PickExecutionIssueService( | |||
| println("=== Final IssueQty Calculation ===") | |||
| println(" Calculated IssueQty: $issueQty") | |||
| println("================================================") | |||
| // 5. 创建 pick execution issue 记录 | |||
| println("=== Processing Logic Selection ===") | |||
| // 6. 创建 pick execution issue 记录 | |||
| val issueNo = generateIssueNo() | |||
| println("Generated issue number: $issueNo") | |||
| val lotNoForIssue = request.lotNo ?: inventoryLotLine?.inventoryLot?.lotNo | |||
| val pickExecutionIssue = PickExecutionIssue( | |||
| id = null, | |||
| @@ -200,8 +240,8 @@ open class PickExecutionIssueService( | |||
| itemId = request.itemId, | |||
| itemCode = request.itemCode, | |||
| itemDescription = request.itemDescription, | |||
| lotId = request.lotId, | |||
| lotNo = request.lotNo, | |||
| lotId = inventoryLotIdForIssue, | |||
| lotNo = lotNoForIssue, | |||
| storeLocation = request.storeLocation, | |||
| requiredQty = request.requiredQty, | |||
| actualPickQty = request.actualPickQty, | |||
| @@ -230,7 +270,7 @@ open class PickExecutionIssueService( | |||
| println(" Handle Status: ${savedIssue.handleStatus}") | |||
| println(" Issue Qty: ${savedIssue.issueQty}") | |||
| // 6. NEW: Update inventory_lot_line.issueQty | |||
| // 7. NEW: Update inventory_lot_line.issueQty(仍然用 lotLineId) | |||
| if (request.lotId != null && inventoryLotLine != null) { | |||
| println("Updating inventory_lot_line.issueQty...") | |||
| // ✅ 修改:如果只有 missQty,不更新 issueQty | |||
| @@ -270,95 +310,19 @@ open class PickExecutionIssueService( | |||
| } | |||
| } | |||
| // 7. 获取相关数据用于后续处理 | |||
| val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO | |||
| val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO | |||
| val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO | |||
| val lotId = request.lotId | |||
| val itemId = request.itemId | |||
| println("=== Processing Logic Selection ===") | |||
| println("Actual Pick Qty: $actualPickQtyForProcessing") | |||
| println("Miss Qty: $missQtyForProcessing") | |||
| println("Bad Item Qty: $badItemQtyForProcessing") | |||
| println("Bad Reason: ${request.badReason}") | |||
| println("Lot ID: $lotId") | |||
| println("Item ID: $itemId") | |||
| println("================================================") | |||
| // 8. 新的统一处理逻辑(根据 badReason 决定处理方式) | |||
| when { | |||
| // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) | |||
| actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { | |||
| println("→ Handling: Miss Item Only") | |||
| handleMissItemOnly(request, missQtyForProcessing) | |||
| } | |||
| // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) | |||
| badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { | |||
| println("→ Handling: Bad Item Only") | |||
| // NEW: Check bad reason | |||
| if (request.badReason == "package_problem") { | |||
| println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem") | |||
| handleBadItemPackageProblem(request, badItemQtyForProcessing) | |||
| } else { | |||
| println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly") | |||
| // quantity_problem or default: handle as normal bad item | |||
| handleBadItemOnly(request, badItemQtyForProcessing) | |||
| } | |||
| } | |||
| // 情况3: 既有 miss item 又有 bad item | |||
| missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { | |||
| println("→ Handling: Both Miss and Bad Item") | |||
| // NEW: Check bad reason | |||
| if (request.badReason == "package_problem") { | |||
| println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem") | |||
| handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing) | |||
| } else { | |||
| println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem") | |||
| handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) | |||
| } | |||
| } | |||
| // 情况4: 有 miss item 的情况(无论 actualPickQty 是多少) | |||
| missQtyForProcessing > BigDecimal.ZERO -> { | |||
| println("→ Handling: Miss Item With Partial Pick") | |||
| handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) | |||
| } | |||
| // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) | |||
| actualPickQtyForProcessing > BigDecimal.ZERO -> { | |||
| println("→ Handling: Normal Pick") | |||
| handleNormalPick(request, actualPickQtyForProcessing) | |||
| } | |||
| else -> { | |||
| println("⚠️ Unknown case: actualPickQty=$actualPickQtyForProcessing, missQty=$missQtyForProcessing, badItemQty=$badItemQtyForProcessing") | |||
| } | |||
| } | |||
| val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null) | |||
| val consoCode = pickOrderForCompletion?.consoCode | |||
| if (!consoCode.isNullOrBlank()) { | |||
| // 优先走原来的 consoCode 逻辑(兼容已有 DO 流程) | |||
| println("🔍 Checking if pick order $consoCode should be completed after lot rejection...") | |||
| try { | |||
| checkAndCompletePickOrder(consoCode) | |||
| } catch (e: Exception) { | |||
| println("⚠️ Error checking pick order completion by consoCode: ${e.message}") | |||
| } | |||
| } else if (pickOrderForCompletion != null) { | |||
| // 🔁 没有 consoCode 的情况:改用 pickOrderId 去检查是否可以完结 | |||
| println("🔍 Checking if pick order ${pickOrderForCompletion.code} (ID=${pickOrderForCompletion.id}) " + | |||
| "should be completed after lot rejection (no consoCode)...") | |||
| try { | |||
| checkAndCompletePickOrderByPickOrderId(pickOrderForCompletion.id!!) | |||
| } catch (e: Exception) { | |||
| println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}") | |||
| } | |||
| // 7. 按规则:issue form 只记录问题 +(可选)把 SOL 标记为 checked | |||
| val stockOutLines = stockOutLineRepository | |||
| .findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||
| request.pickOrderLineId, | |||
| request.lotId ?: 0L | |||
| ) | |||
| stockOutLines.forEach { sol -> | |||
| sol.status = "checked" | |||
| sol.modified = LocalDateTime.now() | |||
| sol.modifiedBy = "system" | |||
| stockOutLineRepository.save(sol) | |||
| } | |||
| stockOutLineRepository.flush() | |||
| println("=== recordPickExecutionIssue: SUCCESS ===") | |||
| println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}") | |||
| @@ -385,6 +349,23 @@ open class PickExecutionIssueService( | |||
| ) | |||
| } | |||
| } | |||
| private fun handleAllZeroMarkCompleted(request: PickExecutionIssueRequest) { | |||
| val stockOutLines = stockOutLineRepository | |||
| .findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||
| request.pickOrderLineId, | |||
| request.lotId ?: 0L | |||
| ) | |||
| stockOutLines.forEach { sol -> | |||
| // issue form 不完结,只标记 checked,让 submit/batch submit 决定 completed(允许 0) | |||
| sol.status = "checked" | |||
| sol.modified = LocalDateTime.now() | |||
| sol.modifiedBy = "system" | |||
| stockOutLineRepository.save(sol) | |||
| println("All-zero case: mark stock out line ${sol.id} as checked (qty kept as ${sol.qty})") | |||
| } | |||
| stockOutLineRepository.flush() | |||
| } | |||
| private fun generateIssueNo(): String { | |||
| val now = LocalDateTime.now() | |||
| val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM")) | |||
| @@ -717,34 +698,18 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac | |||
| // ✅ 修改:不更新 unavailableQty(因为不 reject lot) | |||
| // ✅ 修改:不 reject stock_out_line,根据 actualPickQty 设置状态 | |||
| // ✅ 按规则:issue form 不负责完结/数量提交,只记录问题 +(可选)把 SOL 标记为 checked | |||
| val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||
| request.pickOrderLineId, | |||
| request.lotId ?: 0L | |||
| ) | |||
| stockOutLines.forEach { stockOutLine -> | |||
| val requiredQty = request.requiredQty?.toDouble() ?: 0.0 | |||
| val actualPickQtyDouble = actualPickQty.toDouble() | |||
| // 设置状态:如果 actualPickQty >= requiredQty,则为 completed,否则为 partially_completed | |||
| val newStatus = if (actualPickQtyDouble >= requiredQty) { | |||
| "completed" | |||
| } else { | |||
| "partially_completed" | |||
| } | |||
| stockOutLine.status = newStatus | |||
| stockOutLine.qty = actualPickQtyDouble | |||
| stockOutLine.status = "checked" | |||
| stockOutLine.modified = LocalDateTime.now() | |||
| stockOutLine.modifiedBy = "system" | |||
| val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) | |||
| println("Updated stock out line ${stockOutLine.id} status to: ${newStatus} (NOT rejected)") | |||
| // ✅ 修复:使用更新前的 onHandQty 计算 balance | |||
| val balance = onHandQtyBeforeUpdate - actualPickQtyDouble | |||
| createStockLedgerForStockOut(savedStockOutLine, "Nor", balance) | |||
| stockOutLineRepository.saveAndFlush(stockOutLine) | |||
| println("Issue form: marked stock out line ${stockOutLine.id} as checked (no completion)") | |||
| } | |||
| // ✅ 修复:检查 pick order line 是否应该标记为完成 | |||
| @@ -21,8 +21,10 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderGroup | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderGroupRepository | |||
| import com.ffii.fpsms.modules.pickOrder.web.models.* | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockOut | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutLine | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutRepository | |||
| import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | |||
| @@ -1458,7 +1460,7 @@ open class PickOrderService( | |||
| println("=== DEBUG: checkAndCompletePickOrderByConsoCode ===") | |||
| println("consoCode: $consoCode") | |||
| val stockOut = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null) | |||
| val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(consoCode) | |||
| if (stockOut == null) { | |||
| println("❌ No stock_out found for consoCode: $consoCode") | |||
| return MessageResponse( | |||
| @@ -3357,286 +3359,7 @@ ORDER BY | |||
| val enrichedResults = filteredResults | |||
| return enrichedResults | |||
| } | |||
| // 修改后的逻辑 | |||
| /* | |||
| open fun getAllPickOrderLotsWithDetailsHierarchicalold(userId: Long): Map<String, Any?> { | |||
| println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (Repository-based) ===") | |||
| println("userId filter: $userId") | |||
| val user = userService.find(userId).orElse(null) | |||
| if (user == null) { | |||
| println("❌ User not found: $userId") | |||
| return emptyMap() | |||
| } | |||
| // Step 1:直接按 handledBy 查当前用户的活动 do_pick_order(一个 ticket) | |||
| val activeTicketStatuses = listOf("released", "picking") // 如果你用的是 DoPickOrderStatus 枚举,也可以改成 List<DoPickOrderStatus> | |||
| val doPickOrder = doPickOrderRepository | |||
| .findFirstByHandledByAndDeletedFalseAndTicketStatusIn(user.id!!, activeTicketStatuses) | |||
| if (doPickOrder == null) { | |||
| println("❌ No active do_pick_order found for handledBy user $userId") | |||
| return mapOf( | |||
| "fgInfo" to null, | |||
| "pickOrders" to emptyList<Any>() | |||
| ) | |||
| } | |||
| val doPickOrderId = doPickOrder.id!! | |||
| println(" Using do_pick_order ID (by handledBy): $doPickOrderId") | |||
| // Step 2:用这个 do_pick_orderId 查对应的 do_pick_order_line / pick_order | |||
| val allDoPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrderId) | |||
| val allPickOrderIdsForThisTicket = allDoPickOrderLines.mapNotNull { it.pickOrderId }.distinct() | |||
| println(" Found ${allPickOrderIdsForThisTicket.size} pick orders in this do_pick_order (including completed)") | |||
| // Step 3:加载这些 pick orders(包括 COMPLETED) | |||
| val pickOrders = pickOrderRepository.findAllById(allPickOrderIdsForThisTicket) | |||
| .filter { | |||
| it.deleted == false && | |||
| it.assignTo?.id == userId && | |||
| it.type?.value == "do" | |||
| } | |||
| println(" Loaded ${pickOrders.size} pick orders (including completed)") | |||
| // Step 4:原来你从 3413 行开始的收集所有 line / lots 的逻辑,全部保留 | |||
| val allPickOrderLineIds = pickOrders.flatMap { it.pickOrderLines }.mapNotNull { it.id } | |||
| val allSuggestions = suggestPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIds) | |||
| val allStockOutLines = allPickOrderLineIds.flatMap { lineId -> | |||
| stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(lineId) | |||
| } | |||
| val suggestionsByLineId = allSuggestions.groupBy { spl: SuggestedPickLot -> | |||
| spl.pickOrderLine?.id | |||
| } | |||
| val stockOutLinesByLineId = allStockOutLines.groupBy { sol: StockOutLineInfo -> | |||
| sol.pickOrderLineId | |||
| } | |||
| val allPickOrderLines = mutableListOf<Map<String, Any?>>() | |||
| val lineCountsPerPickOrder = mutableListOf<Int>() | |||
| val pickOrderIdsList = mutableListOf<Long>() | |||
| val pickOrderCodesList = mutableListOf<String>() | |||
| val doOrderIdsList = mutableListOf<Long>() | |||
| val deliveryOrderCodesList = mutableListOf<String>() | |||
| pickOrders.forEach { po -> | |||
| pickOrderIdsList.add(po.id!!) | |||
| pickOrderCodesList.add(po.code ?: "") | |||
| val doOrderId = po.deliveryOrder?.id | |||
| if (doOrderId != null) doOrderIdsList.add(doOrderId) | |||
| deliveryOrderCodesList.add(po.deliveryOrder?.code ?: "") | |||
| val lines = po.pickOrderLines.filter { !it.deleted } | |||
| val lineDtos = po.pickOrderLines | |||
| .filter { !it.deleted } | |||
| .map { pol -> | |||
| val lineId = pol.id | |||
| val item = pol.item | |||
| val uom = pol.uom | |||
| // 获取该 line 的 suggestions 和 stock out lines | |||
| val suggestions = lineId?.let { suggestionsByLineId[it] } ?: emptyList() | |||
| val stockOutLines = lineId?.let { stockOutLinesByLineId[it] } ?: emptyList() | |||
| // 构建 lots(合并相同 lot 的多个 suggestions) | |||
| val lotMap = mutableMapOf<Long?, LotDetailResponse>() | |||
| suggestions.forEach { spl -> | |||
| val ill = spl.suggestedLotLine | |||
| if (ill != null && ill.id != null) { | |||
| val illId = ill.id!! | |||
| val illEntity = inventoryLotLinesMap[illId] ?: ill | |||
| val il = illEntity.inventoryLot | |||
| val w = illEntity.warehouse | |||
| val isExpired = il?.expiryDate?.let { exp -> exp.isBefore(today) } == true | |||
| val availableQty = (illEntity.inQty ?: zero) | |||
| .minus(illEntity.outQty ?: zero) | |||
| .minus(illEntity.holdQty ?: zero) | |||
| // 查找对应的 stock out line | |||
| val stockOutLine = stockOutLines.find { sol -> | |||
| sol.inventoryLotLineId == illId | |||
| } | |||
| // 计算 actualPickQty | |||
| val actualPickQty = stockOutLine?.qty?.let { numToBigDecimal(it as? Number) } | |||
| if (lotMap.containsKey(illId)) { | |||
| // 合并 requiredQty | |||
| val existing = lotMap[illId]!! | |||
| val newRequiredQty = (existing.requiredQty ?: zero).plus(spl.qty ?: zero) | |||
| lotMap[illId] = existing.copy(requiredQty = newRequiredQty) | |||
| } else { | |||
| lotMap[illId] = LotDetailResponse( | |||
| id = illId, | |||
| lotNo = il?.lotNo, | |||
| expiryDate = il?.expiryDate, | |||
| location = w?.code, | |||
| stockUnit = illEntity.stockUom?.uom?.udfudesc ?: uom?.udfudesc ?: "N/A", | |||
| availableQty = availableQty, | |||
| requiredQty = spl.qty, | |||
| actualPickQty = actualPickQty, | |||
| inQty = illEntity.inQty, | |||
| outQty = illEntity.outQty, | |||
| holdQty = illEntity.holdQty, | |||
| lotStatus = illEntity.status?.value, | |||
| lotAvailability = when { | |||
| isExpired -> "expired" | |||
| stockOutLine?.status == "rejected" -> "rejected" | |||
| availableQty <= zero -> "insufficient_stock" | |||
| illEntity.status?.value == "unavailable" -> "status_unavailable" | |||
| else -> "available" | |||
| }, | |||
| processingStatus = when { | |||
| stockOutLine?.status == "completed" -> "completed" | |||
| stockOutLine?.status == "rejected" -> "rejected" | |||
| else -> "pending" | |||
| }, | |||
| suggestedPickLotId = spl.id, | |||
| stockOutLineId = stockOutLine?.id, | |||
| stockOutLineStatus = stockOutLine?.status, | |||
| stockOutLineQty = stockOutLine?.qty?.let { numToBigDecimal(it as? Number) }, | |||
| router = RouterInfoResponse( | |||
| id = null, | |||
| index = w?.order.toString(), | |||
| route = w?.code, | |||
| area = w?.code | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| val lots = lotMap.values.toList() | |||
| // 构建 stockouts(包括没有 lot 的) | |||
| val stockouts = stockOutLines.map { sol -> | |||
| val illId = sol.inventoryLotLineId | |||
| val ill = illId?.let { inventoryLotLinesMap[it] } | |||
| val il = ill?.inventoryLot | |||
| val w = ill?.warehouse | |||
| val available = if (ill == null) null else | |||
| (ill.inQty ?: zero) | |||
| .minus(ill.outQty ?: zero) | |||
| .minus(ill.holdQty ?: zero) | |||
| StockOutDetailResponse( | |||
| id = sol.id, | |||
| status = sol.status, | |||
| qty = sol.qty?.let { numToBigDecimal(it as? Number) }, | |||
| lotId = ill?.id, | |||
| lotNo = il?.lotNo ?: "", | |||
| location = w?.code ?: "", | |||
| availableQty = available, | |||
| noLot = (ill == null) | |||
| ) | |||
| } | |||
| PickOrderLineDetailResponse( | |||
| id = lineId, | |||
| requiredQty = pol.qty, | |||
| status = pol.status?.value, | |||
| item = ItemInfoResponse( | |||
| id = item?.id, | |||
| code = item?.code, | |||
| name = item?.name, | |||
| uomCode = uom?.code, | |||
| uomDesc = uom?.udfudesc, | |||
| uomShortDesc = uom?.udfShortDesc | |||
| ), | |||
| lots = lots, | |||
| stockouts = stockouts | |||
| ) | |||
| } | |||
| lineCountsPerPickOrder.add(lineDtos.size) | |||
| allPickOrderLines.addAll(lineDtos) | |||
| } | |||
| // 排序、fgInfo、mergedPickOrder 这些也全部沿用你当前代码,只要用上面定义好的 doPickOrder/doPickOrderId 即可: | |||
| allPickOrderLines.sortWith(compareBy( | |||
| { line -> | |||
| val lots = line["lots"] as? List<Map<String, Any?>> | |||
| val firstLot = lots?.firstOrNull() | |||
| val router = firstLot?.get("router") as? Map<String, Any?> | |||
| val indexValue = router?.get("index") | |||
| val floorSortValue = when (indexValue) { | |||
| is String -> { | |||
| val parts = indexValue.split("-") | |||
| if (parts.isNotEmpty()) { | |||
| val floorPart = parts[0].uppercase() | |||
| when (floorPart) { | |||
| "1F" -> 1 | |||
| "2F", "4F" -> 2 | |||
| else -> 3 | |||
| } | |||
| } else 3 | |||
| } | |||
| else -> 3 | |||
| } | |||
| floorSortValue | |||
| }, | |||
| { line -> | |||
| val lots = line["lots"] as? List<Map<String, Any?>> | |||
| val firstLot = lots?.firstOrNull() | |||
| val router = firstLot?.get("router") as? Map<String, Any?> | |||
| val indexValue = router?.get("index") | |||
| when (indexValue) { | |||
| is Number -> indexValue.toInt() | |||
| is String -> { | |||
| val parts = indexValue.split("-") | |||
| if (parts.size > 1) { | |||
| parts.last().toIntOrNull() ?: 999999 | |||
| } else { | |||
| indexValue.toIntOrNull() ?: 999999 | |||
| } | |||
| } | |||
| else -> 999999 | |||
| } | |||
| } | |||
| )) | |||
| val fgInfo = mapOf( | |||
| "doPickOrderId" to doPickOrderId, | |||
| "ticketNo" to doPickOrder.ticketNo, | |||
| "storeId" to doPickOrder.storeId, | |||
| "shopCode" to doPickOrder.shopCode, | |||
| "shopName" to doPickOrder.shopName, | |||
| "truckLanceCode" to doPickOrder.truckLanceCode, | |||
| "departureTime" to doPickOrder.truckDepartureTime?.toString() | |||
| ) | |||
| val mergedPickOrder = if (pickOrders.isNotEmpty()) { | |||
| val firstPickOrder = pickOrders.first() | |||
| mapOf( | |||
| "pickOrderIds" to pickOrderIdsList, | |||
| "pickOrderCodes" to pickOrderCodesList, | |||
| "doOrderIds" to doOrderIdsList, | |||
| "deliveryOrderCodes" to deliveryOrderCodesList, | |||
| "lineCountsPerPickOrder" to lineCountsPerPickOrder, | |||
| "consoCodes" to pickOrders.mapNotNull { it.consoCode }.distinct(), | |||
| "status" to doPickOrder.ticketStatus?.value, | |||
| "targetDate" to firstPickOrder.targetDate?.toLocalDate()?.toString(), | |||
| "pickOrderLines" to allPickOrderLines | |||
| ) | |||
| } else { | |||
| null | |||
| } | |||
| return mapOf( | |||
| "fgInfo" to fgInfo, | |||
| "pickOrders" to listOfNotNull(mergedPickOrder) | |||
| ) | |||
| } | |||
| */ | |||
| open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> { | |||
| println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===") | |||
| println("userId filter: $userId") | |||
| @@ -4159,112 +3882,202 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto | |||
| ) | |||
| } | |||
| } | |||
| @Transactional(rollbackFor = [java.lang.Exception::class]) | |||
| open fun confirmLotSubstitution(req: LotSubstitutionConfirmRequest): MessageResponse { | |||
| val zero = BigDecimal.ZERO | |||
| // Validate pick order line | |||
| val pol = req.pickOrderLineId.let { pickOrderLineRepository.findById(it).orElse(null) } | |||
| val pol = pickOrderLineRepository.findById(req.pickOrderLineId).orElse(null) | |||
| ?: return MessageResponse( | |||
| id = null, name = "Pick order line not found", code = "ERROR", type = "pickorder", | |||
| message = "Pick order line ${req.pickOrderLineId} not found", errorPosition = null | |||
| ) | |||
| val polItemId = pol.item?.id | |||
| if (polItemId == null) { | |||
| return MessageResponse( | |||
| ?: return MessageResponse( | |||
| id = null, name = "Item not found", code = "ERROR", type = "pickorder", | |||
| message = "Pick order line item is null", errorPosition = null | |||
| ) | |||
| } | |||
| // ✅ 根据 lotNo 和 itemId 查找新的 InventoryLotLine | |||
| val newIll = when { | |||
| // 优先使用 stockInLineId(更可靠) | |||
| req.newStockInLineId != null && req.newStockInLineId > 0 -> { | |||
| // 通过 stockInLineId 查找 InventoryLot | |||
| val inventoryLot = inventoryLotRepository.findByStockInLineIdAndDeletedFalse(req.newStockInLineId) | |||
| ?: return MessageResponse( | |||
| id = null, name = "Inventory lot not found", code = "ERROR", type = "pickorder", | |||
| message = "Inventory lot with stockInLineId ${req.newStockInLineId} not found", | |||
| errorPosition = null | |||
| ) | |||
| // 通过 InventoryLot 和 itemId 查找 InventoryLotLine | |||
| val lotLines = inventoryLotLineRepository.findAllByInventoryLotId(inventoryLot.id!!) | |||
| .filter { it.inventoryLot?.item?.id == polItemId && !it.deleted } | |||
| if (lotLines.isEmpty()) { | |||
| return MessageResponse( | |||
| id = null, name = "Lot line not found", code = "ERROR", type = "pickorder", | |||
| message = "Inventory lot line with stockInLineId ${req.newStockInLineId} and itemId ${polItemId} not found", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| // 如果有多个,取第一个(通常应该只有一个) | |||
| lotLines.first() | |||
| } | |||
| // 兼容旧方式:使用 lotNo | |||
| req.newInventoryLotNo != null && req.newInventoryLotNo.isNotBlank() -> { | |||
| inventoryLotLineRepository.findByLotNoAndItemId(req.newInventoryLotNo, polItemId) | |||
| ?: return MessageResponse( | |||
| id = null, name = "New lot line not found", code = "ERROR", type = "pickorder", | |||
| message = "Inventory lot line with lotNo '${req.newInventoryLotNo}' and itemId ${polItemId} not found", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| else -> { | |||
| return MessageResponse( | |||
| id = null, name = "Invalid request", code = "ERROR", type = "pickorder", | |||
| message = "Either newStockInLineId or newInventoryLotNo must be provided", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| } | |||
| // Find new InventoryLotLine (from stockInLineId first, fallback lotNo) | |||
| val newIll = resolveNewInventoryLotLine(req, polItemId) | |||
| ?: return MessageResponse( | |||
| id = null, name = "New lot line not found", code = "ERROR", type = "pickorder", | |||
| message = "Cannot resolve new inventory lot line", errorPosition = null | |||
| ) | |||
| // Item consistency check (应该已经通过上面的查询保证了,但再次确认) | |||
| // Item consistency check | |||
| val newItemId = newIll.inventoryLot?.item?.id | |||
| if (newItemId == null || polItemId != newItemId) { | |||
| if (newItemId == null || newItemId != polItemId) { | |||
| return MessageResponse( | |||
| id = null, name = "Item mismatch", code = "ERROR", type = "pickorder", | |||
| message = "New lot line item does not match pick order line item", errorPosition = null | |||
| ) | |||
| } | |||
| val newIllId = newIll.id ?: return MessageResponse( | |||
| id = null, name = "Invalid lot line", code = "ERROR", type = "pickorder", | |||
| message = "New inventory lot line has no ID", errorPosition = null | |||
| ) | |||
| val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo ?: "unknown" | |||
| // 1) Update suggested pick lot (if provided): move holdQty from old ILL to new ILL and re-point the suggestion | |||
| if (req.originalSuggestedPickLotId != null && req.originalSuggestedPickLotId > 0) { | |||
| // ✅ 使用 repository 而不是 SQL | |||
| val originalSpl = suggestPickLotRepository.findById(req.originalSuggestedPickLotId).orElse(null) | |||
| if (originalSpl != null) { | |||
| val oldIll = originalSpl.suggestedLotLine | |||
| val qty = originalSpl.qty ?: zero | |||
| // Resolve SuggestedPickLot: | |||
| // - If originalSuggestedPickLotId provided: use it | |||
| // - Else (1:1 assumption): find by pickOrderLineId (optionally also by stockOutLineId if you add repository method) | |||
| val spl = if (req.originalSuggestedPickLotId != null && req.originalSuggestedPickLotId > 0) { | |||
| suggestPickLotRepository.findById(req.originalSuggestedPickLotId).orElse(null) | |||
| } else { | |||
| // 1:1 assumption fallback (you need a repository method; replace with your actual one) | |||
| // e.g. suggestPickLotRepository.findFirstByPickOrderLineIdAndDeletedFalseOrderByIdDesc(req.pickOrderLineId) | |||
| suggestPickLotRepository.findFirstByPickOrderLineId(req.pickOrderLineId) | |||
| } | |||
| if (oldIll != null && oldIll.id != newIllId) { | |||
| // Decrease hold on old, increase on new | |||
| oldIll.holdQty = (oldIll.holdQty ?: zero).minus(qty).max(zero) | |||
| inventoryLotLineRepository.save(oldIll) | |||
| newIll.holdQty = (newIll.holdQty ?: zero).plus(qty) | |||
| inventoryLotLineRepository.save(newIll) | |||
| } | |||
| if (spl == null) { | |||
| return MessageResponse( | |||
| id = null, name = "Suggested pick lot not found", code = "ERROR", type = "pickorder", | |||
| message = "SuggestedPickLot not found for pickOrderLineId=${req.pickOrderLineId}", errorPosition = null | |||
| ) | |||
| } | |||
| val qtyToHold = spl.qty ?: zero | |||
| if (qtyToHold.compareTo(zero) <= 0) { | |||
| return MessageResponse( | |||
| id = null, name = "Invalid qty", code = "ERROR", type = "pickorder", | |||
| message = "SuggestedPickLot qty is invalid: $qtyToHold", errorPosition = null | |||
| ) | |||
| } | |||
| // Availability check on newIll BEFORE updates | |||
| val inQty = newIll.inQty ?: zero | |||
| val outQty = newIll.outQty ?: zero | |||
| val holdQty = newIll.holdQty ?: zero | |||
| val issueQty = newIll.issueQty ?: zero | |||
| val available = inQty.subtract(outQty).subtract(holdQty).subtract(issueQty) | |||
| if (available.compareTo(qtyToHold) < 0) { | |||
| return MessageResponse( | |||
| id = null, name = "Insufficient lot qty", code = "REJECT", type = "pickorder", | |||
| message = "Reject switch lot: available=$available < required=$qtyToHold", errorPosition = null | |||
| ) | |||
| } | |||
| // ✅ 使用 repository 更新 suggestion | |||
| originalSpl.suggestedLotLine = newIll | |||
| suggestPickLotRepository.save(originalSpl) | |||
| val oldIll = spl.suggestedLotLine | |||
| // Load stock out line (if provided) to decide "bind vs split" | |||
| val existingSol = if (req.stockOutLineId != null && req.stockOutLineId > 0) { | |||
| stockOutLIneRepository.findById(req.stockOutLineId).orElse(null) | |||
| } else null | |||
| val pickedQty = existingSol?.qty?.let { numToBigDecimal(it as? Number) } ?: zero | |||
| // Switch lot rule: | |||
| // - actual pick == 0: replace/bind (no new line) | |||
| // - actual pick > 0: split remaining qty into a NEW suggested pick lot + stock out line | |||
| if (pickedQty.compareTo(zero) > 0) { | |||
| val remaining = qtyToHold.subtract(pickedQty) | |||
| if (remaining.compareTo(zero) <= 0) { | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "No remaining qty", | |||
| code = "REJECT", | |||
| type = "pickorder", | |||
| message = "Reject switch lot: picked=$pickedQty already >= required=$qtyToHold", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| // Move HOLD for remaining qty (old -> new) | |||
| if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) { | |||
| val oldHold = oldIll.holdQty ?: zero | |||
| val newOldHold = oldHold.subtract(remaining) | |||
| oldIll.holdQty = if (newOldHold.compareTo(zero) < 0) zero else newOldHold | |||
| inventoryLotLineRepository.save(oldIll) | |||
| val newHold = (newIll.holdQty ?: zero).add(remaining) | |||
| newIll.holdQty = newHold | |||
| inventoryLotLineRepository.save(newIll) | |||
| } | |||
| if (oldIll == null) { | |||
| val newHold = (newIll.holdQty ?: zero).add(remaining) | |||
| newIll.holdQty = newHold | |||
| inventoryLotLineRepository.save(newIll) | |||
| } | |||
| // Lock current suggestion qty to the picked qty (picked part stays on oldIll) | |||
| spl.qty = pickedQty | |||
| suggestPickLotRepository.saveAndFlush(spl) | |||
| // Create a NEW stock out line + suggestion for the remaining qty | |||
| val stockOut: StockOut = if (existingSol != null) { | |||
| existingSol.stockOut ?: throw IllegalStateException("Existing StockOutLine has null stockOut") | |||
| } else { | |||
| val consoCode = pol.pickOrder?.consoCode ?: "" | |||
| val existing = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null) | |||
| if (existing != null) { | |||
| existing | |||
| } else { | |||
| val handlerId = pol.pickOrder?.assignTo?.id ?: SecurityUtils.getUser().orElse(null)?.id | |||
| require(handlerId != null) { "Cannot create StockOut: handlerId is null" } | |||
| val newStockOut = StockOut().apply { | |||
| this.consoPickOrderCode = consoCode | |||
| this.type = pol.pickOrder?.type?.value ?: "" | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = handlerId | |||
| } | |||
| stockOutRepository.save(newStockOut) | |||
| } | |||
| } | |||
| val newSol = StockOutLine().apply { | |||
| this.stockOut = stockOut | |||
| this.pickOrderLine = pol | |||
| this.item = pol.item | |||
| this.inventoryLotLine = newIll | |||
| this.qty = 0.0 | |||
| this.status = StockOutLineStatus.CHECKED.status | |||
| this.startTime = LocalDateTime.now() | |||
| this.type = existingSol?.type ?: "Nor" | |||
| } | |||
| stockOutLIneRepository.saveAndFlush(newSol) | |||
| val newSpl = SuggestedPickLot().apply { | |||
| this.type = spl.type | |||
| this.pickOrderLine = pol | |||
| this.suggestedLotLine = newIll | |||
| this.stockOutLine = newSol | |||
| this.qty = remaining | |||
| this.pickSuggested = spl.pickSuggested | |||
| } | |||
| suggestPickLotRepository.saveAndFlush(newSpl) | |||
| val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "Lot substitution confirmed (split line)", | |||
| code = "SUCCESS", | |||
| type = "pickorder", | |||
| message = "Picked=$pickedQty, created new line for remaining=$remaining on lotNo '$newLotNo'", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| // If oldIll exists and different: move hold old -> new | |||
| if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) { | |||
| val oldHold = oldIll.holdQty ?: zero | |||
| val newOldHold = oldHold.subtract(qtyToHold) | |||
| oldIll.holdQty = if (newOldHold.compareTo(zero) < 0) zero else newOldHold | |||
| inventoryLotLineRepository.save(oldIll) | |||
| val newHold = (newIll.holdQty ?: zero).add(qtyToHold) | |||
| newIll.holdQty = newHold | |||
| inventoryLotLineRepository.save(newIll) | |||
| } | |||
| // If first bind (oldIll == null): just hold on new | |||
| if (oldIll == null) { | |||
| val newHold = (newIll.holdQty ?: zero).add(qtyToHold) | |||
| newIll.holdQty = newHold | |||
| inventoryLotLineRepository.save(newIll) | |||
| } | |||
| // 2) Update stock out line (if provided): re-point to new ILL; keep qty and status unchanged | |||
| // Point suggestion to new lot line | |||
| spl.suggestedLotLine = newIll | |||
| suggestPickLotRepository.save(spl) | |||
| // Update stock out line if provided | |||
| if (req.stockOutLineId != null && req.stockOutLineId > 0) { | |||
| val sol = stockOutLIneRepository.findById(req.stockOutLineId).orElse(null) | |||
| if (sol != null) { | |||
| @@ -4274,15 +4087,43 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto | |||
| } | |||
| } | |||
| val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "Lot substitution confirmed", | |||
| code = "SUCCESS", | |||
| type = "pickorder", | |||
| message = "Updated suggestion and stock out line to new lot line with lotNo '${newLotNo}'", | |||
| errorPosition = null | |||
| message = "Updated suggestion and stock out line to new lot line with lotNo '$newLotNo'", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| private fun resolveNewInventoryLotLine( | |||
| req: LotSubstitutionConfirmRequest, | |||
| itemId: Long | |||
| ): InventoryLotLine? { | |||
| // Prefer stockInLineId | |||
| if (req.newStockInLineId != null && req.newStockInLineId > 0) { | |||
| val inventoryLot = inventoryLotRepository.findByStockInLineIdAndDeletedFalse(req.newStockInLineId) | |||
| ?: return null | |||
| val lotLines = inventoryLotLineRepository.findAllByInventoryLotId(inventoryLot.id!!) | |||
| .filter { it.inventoryLot?.item?.id == itemId && !it.deleted } | |||
| return lotLines.firstOrNull() | |||
| } | |||
| // Fallback lotNo (req.newInventoryLotNo is non-null String in your model) | |||
| if (req.newInventoryLotNo.isNotBlank()) { | |||
| return inventoryLotLineRepository | |||
| .findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc( | |||
| req.newInventoryLotNo, | |||
| itemId | |||
| ) | |||
| } | |||
| return null | |||
| } | |||
| open fun getCompletedDoPickOrders( | |||
| userId: Long, | |||
| @@ -815,6 +815,107 @@ fun searchMaterialStockOutTraceabilityReport( | |||
| return jdbcDao.queryForList(sql, args) | |||
| } | |||
| /** | |||
| * GRN (Goods Received Note) report: stock-in lines with PO/delivery note, filterable by receipt date range and item code. | |||
| * Returns rows for Excel export: poCode, deliveryNoteNo, receiptDate, itemCode, itemName, acceptedQty, demandQty, uom, etc. | |||
| */ | |||
| fun searchGrnReport( | |||
| receiptDateStart: String?, | |||
| receiptDateEnd: String?, | |||
| itemCode: String? | |||
| ): List<Map<String, Any?>> { | |||
| val args = mutableMapOf<String, Any>() | |||
| val receiptDateStartSql = if (!receiptDateStart.isNullOrBlank()) { | |||
| val formatted = receiptDateStart.replace("/", "-") | |||
| args["receiptDateStart"] = formatted | |||
| "AND DATE(sil.receiptDate) >= DATE(:receiptDateStart)" | |||
| } else "" | |||
| val receiptDateEndSql = if (!receiptDateEnd.isNullOrBlank()) { | |||
| val formatted = receiptDateEnd.replace("/", "-") | |||
| args["receiptDateEnd"] = formatted | |||
| "AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)" | |||
| } else "" | |||
| val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args) | |||
| val sql = """ | |||
| SELECT | |||
| po.code AS poCode, | |||
| CASE | |||
| WHEN sil.dnNo = 'DN00000' OR sil.dnNo IS NULL THEN '' | |||
| ELSE sil.dnNo | |||
| END AS deliveryNoteNo, | |||
| DATE_FORMAT(sil.receiptDate, '%Y-%m-%d') AS receiptDate, | |||
| COALESCE(it.code, '') AS itemCode, | |||
| COALESCE(it.name, '') AS itemName, | |||
| COALESCE(sil.acceptedQty, 0) AS acceptedQty, | |||
| COALESCE(sil.demandQty, 0) AS demandQty, | |||
| COALESCE(uc_stock.udfudesc, uc_pol.udfudesc, '') AS uom, | |||
| COALESCE(uc_pol.udfudesc, '') AS purchaseUomDesc, | |||
| COALESCE(uc_stock.udfudesc, '') AS stockUomDesc, | |||
| COALESCE(sil.productLotNo, '') AS productLotNo, | |||
| DATE_FORMAT(sil.expiryDate, '%Y-%m-%d') AS expiryDate, | |||
| COALESCE(sp.code, '') AS supplierCode, | |||
| COALESCE(sp.name, '') AS supplier, | |||
| COALESCE(sil.status, '') AS status, | |||
| MAX(grn.m18_record_id) AS grnId | |||
| FROM stock_in_line sil | |||
| LEFT JOIN items it ON sil.itemId = it.id | |||
| LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id | |||
| LEFT JOIN shop sp ON po.supplierId = sp.id | |||
| LEFT JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id | |||
| LEFT JOIN uom_conversion uc_pol ON pol.uomId = uc_pol.id | |||
| LEFT JOIN item_uom iu_stock ON it.id = iu_stock.itemId AND iu_stock.stockUnit = true AND iu_stock.deleted = false | |||
| LEFT JOIN uom_conversion uc_stock ON iu_stock.uomId = uc_stock.id | |||
| LEFT JOIN m18_goods_receipt_note_log grn | |||
| ON grn.stock_in_line_id = sil.id | |||
| WHERE sil.deleted = false | |||
| AND sil.receiptDate IS NOT NULL | |||
| AND sil.purchaseOrderId IS NOT NULL | |||
| $receiptDateStartSql | |||
| $receiptDateEndSql | |||
| $itemCodeSql | |||
| GROUP BY | |||
| po.code, | |||
| deliveryNoteNo, | |||
| receiptDate, | |||
| itemCode, | |||
| itemName, | |||
| acceptedQty, | |||
| demandQty, | |||
| uom, | |||
| purchaseUomDesc, | |||
| stockUomDesc, | |||
| productLotNo, | |||
| expiryDate, | |||
| supplierCode, | |||
| supplier, | |||
| status | |||
| ORDER BY sil.receiptDate, po.code, sil.id | |||
| """.trimIndent() | |||
| val rows = jdbcDao.queryForList(sql, args) | |||
| return rows.map { row -> | |||
| mapOf( | |||
| "poCode" to row["poCode"], | |||
| "deliveryNoteNo" to row["deliveryNoteNo"], | |||
| "receiptDate" to row["receiptDate"], | |||
| "itemCode" to row["itemCode"], | |||
| "itemName" to row["itemName"], | |||
| "acceptedQty" to (row["acceptedQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0), | |||
| "receivedQty" to (row["acceptedQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0), | |||
| "demandQty" to (row["demandQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0), | |||
| "uom" to row["uom"], | |||
| "purchaseUomDesc" to row["purchaseUomDesc"], | |||
| "stockUomDesc" to row["stockUomDesc"], | |||
| "productLotNo" to row["productLotNo"], | |||
| "expiryDate" to row["expiryDate"], | |||
| "supplierCode" to row["supplierCode"], | |||
| "supplier" to row["supplier"], | |||
| "status" to row["status"], | |||
| "grnId" to row["grnId"] | |||
| ) | |||
| } | |||
| } | |||
| /** | |||
| * Queries the database for Stock Balance Report data (one summarized row per item). | |||
| * Uses stock_ledger with report period (fromDate/toDate): opening = before fromDate, cum in/out = in period, current = up to toDate. | |||
| @@ -320,4 +320,18 @@ class ReportController( | |||
| return ResponseEntity(pdfBytes, headers, HttpStatus.OK) | |||
| } | |||
| /** | |||
| * GRN (Goods Received Note) report data for Excel export. | |||
| * Query by receipt date range and optional item code. Returns JSON { "rows": [ ... ] }. | |||
| */ | |||
| @GetMapping("/grn-report") | |||
| fun getGrnReport( | |||
| @RequestParam(required = false) receiptDateStart: String?, | |||
| @RequestParam(required = false) receiptDateEnd: String?, | |||
| @RequestParam(required = false) itemCode: String? | |||
| ): Map<String, Any> { | |||
| val rows = reportService.searchGrnReport(receiptDateStart, receiptDateEnd, itemCode) | |||
| return mapOf("rows" to rows) | |||
| } | |||
| } | |||
| @@ -53,6 +53,7 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||
| """) | |||
| fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLotLine? | |||
| <<<<<<< HEAD | |||
| @Query(""" | |||
| SELECT DISTINCT ill.inventoryLot.item.id | |||
| FROM InventoryLotLine ill | |||
| @@ -60,6 +61,13 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||
| AND ill.inventoryLot.lotNo = :lotNo | |||
| """) | |||
| fun findDistinctItemIdsByLotNo(@Param("lotNo") lotNo: String): List<Long> | |||
| ======= | |||
| // lotNo + itemId may not be unique (multiple warehouses/lines); pick one deterministically | |||
| fun findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc( | |||
| lotNo: String, | |||
| itemId: Long | |||
| ): InventoryLotLine? | |||
| >>>>>>> 9760717ed6a5c59383467921464fb2b89a7f85a8 | |||
| // InventoryLotLineRepository.kt 中添加 | |||
| @Query("SELECT ill FROM InventoryLotLine ill WHERE ill.warehouse.id IN :warehouseIds AND ill.deleted = false") | |||
| fun findAllByWarehouseIdInAndDeletedIsFalse(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine> | |||
| @@ -7,5 +7,7 @@ import java.util.Optional | |||
| @Repository | |||
| interface StockOutRepository: AbstractRepository<StockOut, Long> { | |||
| fun findByConsoPickOrderCode(consoPickOrderCode: String) : Optional<StockOut> | |||
| // consoPickOrderCode 可能在 DB 中存在重复,避免 single-result exception | |||
| fun findFirstByConsoPickOrderCodeOrderByIdDesc(consoPickOrderCode: String): StockOut? | |||
| fun findByStockTakeIdAndDeletedFalse(stockTakeId: Long): StockOut? | |||
| } | |||
| @@ -8,7 +8,9 @@ import org.springframework.stereotype.Repository | |||
| interface SuggestPickLotRepository : AbstractRepository<SuggestedPickLot, Long> { | |||
| fun findAllByPickOrderLineIn(lines: List<PickOrderLine>): List<SuggestedPickLot> | |||
| fun findAllByPickOrderLineIdIn(pickOrderLineIds: List<Long>): List<SuggestedPickLot> | |||
| fun findFirstByPickOrderLineId(pickOrderLineId: Long): SuggestedPickLot? | |||
| fun findAllByPickOrderLineId(pickOrderLineId: Long): List<SuggestedPickLot> | |||
| fun findFirstByStockOutLineId(stockOutLineId: Long): SuggestedPickLot? | |||
| } | |||
| @@ -43,6 +43,8 @@ import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository | |||
| import java.time.LocalTime | |||
| import com.ffii.fpsms.modules.stock.entity.StockInLineRepository | |||
| import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository | |||
| @Service | |||
| open class StockOutLineService( | |||
| private val jdbcDao: JdbcDao, | |||
| @@ -53,7 +55,9 @@ open class StockOutLineService( | |||
| private val itemUomRespository: ItemUomRespository, | |||
| private val pickOrderRepository: PickOrderRepository, | |||
| private val inventoryLotLineRepository: InventoryLotLineRepository, | |||
| private val stockInLineRepository: StockInLineRepository, | |||
| @Lazy private val suggestedPickLotService: SuggestedPickLotService, | |||
| private val suggestPickLotRepository: SuggestPickLotRepository, | |||
| private val inventoryLotRepository: InventoryLotRepository, | |||
| private val doPickOrderRepository: DoPickOrderRepository, | |||
| private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||
| @@ -68,6 +72,35 @@ private val inventoryLotLineService: InventoryLotLineService, | |||
| private val inventoryRepository: InventoryRepository, | |||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository | |||
| ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { | |||
| private fun isEndStatus(status: String?): Boolean { | |||
| val s = status?.trim()?.lowercase() ?: return false | |||
| return s == "completed" || s == "rejected" || s == "partially_completed" | |||
| } | |||
| @Transactional | |||
| private fun tryCompletePickOrderLine(pickOrderLineId: Long) { | |||
| val sols = stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) | |||
| if (sols.isEmpty()) return | |||
| val allEnded = sols.all { isEndStatus(it.status) } | |||
| if (!allEnded) return | |||
| val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return | |||
| if (pol.status != PickOrderLineStatus.COMPLETED) { | |||
| pol.status = PickOrderLineStatus.COMPLETED | |||
| pickOrderLineRepository.save(pol) | |||
| } | |||
| // Optionally bubble up to pick order completion (safe no-op if not ready) | |||
| val consoCode = pol.pickOrder?.consoCode | |||
| if (!consoCode.isNullOrBlank()) { | |||
| try { | |||
| pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode) | |||
| } catch (e: Exception) { | |||
| println("⚠️ Error checking pick order completion for consoCode=$consoCode: ${e.message}") | |||
| } | |||
| } | |||
| } | |||
| @Throws(IOException::class) | |||
| @Transactional | |||
| open fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> { | |||
| @@ -620,6 +653,10 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { | |||
| } | |||
| val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) | |||
| println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") | |||
| // If this stock out line is in end status, try completing its pick order line | |||
| if (isEndStatus(savedStockOutLine.status)) { | |||
| savedStockOutLine.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) } | |||
| } | |||
| try { | |||
| val item = savedStockOutLine.item | |||
| val inventoryLotLine = savedStockOutLine.inventoryLotLine | |||
| @@ -946,22 +983,62 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| // Step 2: Get InventoryLotLine | |||
| val getInventoryLotLineStart = System.currentTimeMillis() | |||
| // 修复:从 stockOutLine.inventoryLotLine 获取 inventoryLot,而不是使用错误的参数 | |||
| val inventoryLotLine = stockOutLine.inventoryLotLine | |||
| // If StockOutLine has no lot (noLot row), resolve InventoryLotLine by scanned lotNo + itemId and bind it | |||
| var inventoryLotLine = stockOutLine.inventoryLotLine | |||
| if (inventoryLotLine == null) { | |||
| // Prefer stockInLineId from QR for deterministic binding | |||
| val resolved = if (request.stockInLineId != null && request.stockInLineId > 0) { | |||
| println(" Resolving InventoryLotLine by stockInLineId=${request.stockInLineId} ...") | |||
| val sil = stockInLineRepository.findById(request.stockInLineId).orElse(null) | |||
| val ill = sil?.inventoryLotLine | |||
| if (ill == null) { | |||
| println(" StockInLine ${request.stockInLineId} has no associated InventoryLotLine") | |||
| null | |||
| } else { | |||
| // item consistency guard | |||
| val illItemId = ill.inventoryLot?.item?.id | |||
| if (illItemId != null && illItemId != request.itemId) { | |||
| println(" InventoryLotLine item mismatch for stockInLineId=${request.stockInLineId}: $illItemId != ${request.itemId}") | |||
| null | |||
| } else { | |||
| ill | |||
| } | |||
| } | |||
| } else { | |||
| println(" StockOutLine has no associated InventoryLotLine, resolving by lotNo+itemId...") | |||
| inventoryLotLineRepository | |||
| .findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc( | |||
| request.inventoryLotNo, | |||
| request.itemId | |||
| ) | |||
| } | |||
| if (resolved == null) { | |||
| println(" Cannot resolve InventoryLotLine by lotNo=${request.inventoryLotNo}, itemId=${request.itemId}") | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "No inventory lot line", | |||
| code = "NO_INVENTORY_LOT_LINE", | |||
| type = "error", | |||
| message = "Cannot resolve InventoryLotLine (stockInLineId=${request.stockInLineId ?: "null"}, lotNo=${request.inventoryLotNo}, itemId=${request.itemId})", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| // Bind the lot line to this stockOutLine so subsequent operations can proceed | |||
| stockOutLine.inventoryLotLine = resolved | |||
| stockOutLine.item = stockOutLine.item ?: resolved.inventoryLot?.item | |||
| inventoryLotLine = resolved | |||
| // Also update SuggestedPickLot to point to the resolved lot line (so UI/holdQty logic matches DO confirmLotSubstitution) | |||
| val spl = suggestPickLotRepository.findFirstByStockOutLineId(stockOutLine.id!!) | |||
| if (spl != null) { | |||
| spl.suggestedLotLine = resolved | |||
| suggestPickLotRepository.saveAndFlush(spl) | |||
| } | |||
| } | |||
| val getInventoryLotLineTime = System.currentTimeMillis() - getInventoryLotLineStart | |||
| println("⏱️ [STEP 2] Get InventoryLotLine: ${getInventoryLotLineTime}ms") | |||
| if (inventoryLotLine == null) { | |||
| println(" StockOutLine has no associated InventoryLotLine") | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "No inventory lot line", | |||
| code = "NO_INVENTORY_LOT_LINE", | |||
| type = "error", | |||
| message = "StockOutLine ${request.stockOutLineId} has no associated InventoryLotLine", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| // inventoryLotLine is guaranteed non-null here | |||
| // Step 3: Get InventoryLot | |||
| val getInventoryLotStart = System.currentTimeMillis() | |||
| @@ -40,6 +40,7 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo | |||
| import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository | |||
| import com.ffii.fpsms.modules.stock.web.model.StockOutStatus | |||
| import com.ffii.fpsms.modules.common.SecurityUtils | |||
| @Service | |||
| open class SuggestedPickLotService( | |||
| val suggestedPickLotRepository: SuggestPickLotRepository, | |||
| @@ -433,7 +434,32 @@ open class SuggestedPickLotService( | |||
| } | |||
| open fun saveAll(request: List<SuggestedPickLot>): List<SuggestedPickLot> { | |||
| return suggestedPickLotRepository.saveAllAndFlush(request) | |||
| val saved = suggestedPickLotRepository.saveAllAndFlush(request) | |||
| // For insufficient stock (suggestedLotLine == null), create a no-lot stock_out_line so UI can display & close the line. | |||
| // Also backfill SuggestedPickLot.stockOutLineId for downstream flows (e.g. hierarchical API -> stockouts). | |||
| val toBackfill = saved.filter { it.suggestedLotLine == null && it.pickOrderLine != null } | |||
| if (toBackfill.isNotEmpty()) { | |||
| val updated = mutableListOf<SuggestedPickLot>() | |||
| toBackfill.forEach { spl -> | |||
| val pickOrder = spl.pickOrderLine?.pickOrder | |||
| if (pickOrder == null) return@forEach | |||
| // Only create/backfill when stockOutLine is missing | |||
| if (spl.stockOutLine == null) { | |||
| val sol = createStockOutLineForSuggestion(spl, pickOrder) | |||
| if (sol != null) { | |||
| spl.stockOutLine = sol | |||
| updated.add(spl) | |||
| } | |||
| } | |||
| } | |||
| if (updated.isNotEmpty()) { | |||
| suggestedPickLotRepository.saveAllAndFlush(updated) | |||
| } | |||
| } | |||
| return saved | |||
| } | |||
| private fun createStockOutLineForSuggestion( | |||
| suggestion: SuggestedPickLot, | |||
| @@ -470,10 +496,13 @@ open class SuggestedPickLotService( | |||
| // Get or create StockOut | |||
| val stockOut = stockOutRepository.findByConsoPickOrderCode(pickOrder.consoCode ?: "") | |||
| .orElseGet { | |||
| val handlerId = pickOrder.assignTo?.id ?: SecurityUtils.getUser().orElse(null)?.id | |||
| require(handlerId != null) { "Cannot create StockOut: handlerId is null" } | |||
| val newStockOut = StockOut().apply { | |||
| this.consoPickOrderCode = pickOrder.consoCode ?: "" | |||
| this.type = pickOrderTypeValue // Use pick order type (do, job, material, etc.) | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = handlerId | |||
| } | |||
| stockOutRepository.save(newStockOut) | |||
| } | |||
| @@ -484,7 +513,8 @@ open class SuggestedPickLotService( | |||
| this.pickOrderLine = pickOrderLine | |||
| this.item = item | |||
| this.inventoryLotLine = null // No lot available | |||
| this.qty = (suggestion.qty ?: BigDecimal.ZERO).toDouble() | |||
| // qty on StockOutLine represents picked qty; for no-lot placeholder it must start from 0 | |||
| this.qty = 0.0 | |||
| this.status = StockOutLineStatus.PENDING.status | |||
| this.deleted = false | |||
| this.type = "Nor" | |||
| @@ -64,6 +64,7 @@ data class UpdateStockOutLineStatusRequest( | |||
| data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( | |||
| val pickOrderLineId: Long, | |||
| val inventoryLotNo: String, | |||
| val stockInLineId: Long? = null, | |||
| val stockOutLineId: Long, | |||
| val itemId: Long, | |||
| val status: String | |||