| @@ -494,7 +494,8 @@ open class JoPickOrderService( | |||
| uomShortDesc = uom?.udfShortDesc, | |||
| matchStatus = jpo?.matchStatus?.value, | |||
| matchBy = jpo?.matchBy, | |||
| matchQty = jpo?.matchQty?.toDouble() | |||
| matchQty = jpo?.matchQty?.toDouble(), | |||
| stockInLineId = null, | |||
| ) | |||
| } | |||
| @@ -1914,6 +1915,15 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||
| stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId) | |||
| } | |||
| // 获取 stock in lines 通过 inventoryLotLineId(用于填充 stockInLineId) | |||
| val stockInLinesByInventoryLotLineId = if (inventoryLotLineIds.isNotEmpty()) { | |||
| inventoryLotLineIds.associateWith { illId -> | |||
| stockInLineRepository.findStockInLineInfoByInventoryLotLineId(illId).orElse(null)?.id | |||
| }.filterValues { it != null } | |||
| } else { | |||
| emptyMap() | |||
| } | |||
| // 获取 jo_pick_order 记录 | |||
| val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!) | |||
| @@ -1983,6 +1993,11 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||
| else -> "pending" | |||
| } | |||
| // 获取 stockInLineId | |||
| val stockInLineId = ill.id?.let { illId -> | |||
| stockInLinesByInventoryLotLineId[illId] | |||
| } | |||
| LotDetailResponse( | |||
| lotId = ill.id, | |||
| lotNo = il.lotNo, | |||
| @@ -2000,6 +2015,7 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||
| pickOrderConsoCode = pickOrder.consoCode, | |||
| pickOrderLineId = pol.id, | |||
| stockOutLineId = sol?.id, | |||
| stockInLineId = stockInLineId, | |||
| suggestedPickLotId = spl.id, | |||
| stockOutLineQty = sol?.qty ?: 0.0, | |||
| stockOutLineStatus = sol?.status, | |||
| @@ -109,6 +109,7 @@ data class LotDetailResponse( | |||
| val pickOrderConsoCode: String?, | |||
| val pickOrderLineId: Long?, | |||
| val stockOutLineId: Long?, | |||
| val stockInLineId: Long?, | |||
| val suggestedPickLotId: Long?, | |||
| val stockOutLineQty: Double?, | |||
| val stockOutLineStatus: String?, | |||
| @@ -590,7 +590,7 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD | |||
| val newStatus = if (actualPickQtyDouble >= requiredQty) { | |||
| "completed" | |||
| } else { | |||
| "partially_completed" | |||
| "completed" | |||
| } | |||
| stockOutLine.status = newStatus | |||
| @@ -606,6 +606,42 @@ private fun handleMissItemOnly(request: PickExecutionIssueRequest, missQty: BigD | |||
| createStockLedgerForStockOut(savedStockOutLine, "Nor") | |||
| } | |||
| } | |||
| // ✅ NEW: If all stock_out_line under this pick order line are finished (completed/rejected), | |||
| // mark pick_order_line as COMPLETED. This is required for "miss all required qty" flows | |||
| // where qty can be 0 but picking decision is finalized via issue form. | |||
| try { | |||
| stockOutLineRepository.flush() | |||
| val allStockOutLines = stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(request.pickOrderLineId) | |||
| val unfinishedLines = allStockOutLines.filter { | |||
| it.status != StockOutLineStatus.COMPLETE.status && | |||
| it.status != StockOutLineStatus.REJECTED.status | |||
| } | |||
| println("Miss item: Checking pick order line ${request.pickOrderLineId}") | |||
| println(" Total stock out lines: ${allStockOutLines.size}") | |||
| println(" Unfinished lines: ${unfinishedLines.size}") | |||
| unfinishedLines.forEach { line -> | |||
| println(" - Line ${line.id}: status=${line.status}") | |||
| } | |||
| if (unfinishedLines.isEmpty() && allStockOutLines.isNotEmpty()) { | |||
| val pickOrderLine = pickOrderLineRepository.findById(request.pickOrderLineId).orElse(null) | |||
| if (pickOrderLine != null) { | |||
| pickOrderLine.status = PickOrderLineStatus.COMPLETED | |||
| pickOrderLineRepository.saveAndFlush(pickOrderLine) | |||
| println("✅ Miss item: Updated pick order line ${request.pickOrderLineId} status to COMPLETED") | |||
| } else { | |||
| println("⚠️ Miss item: Pick order line ${request.pickOrderLineId} not found") | |||
| } | |||
| } else { | |||
| println("⚠️ Miss item: Pick order line ${request.pickOrderLineId} still has ${unfinishedLines.size} unfinished lines") | |||
| } | |||
| } catch (e: Exception) { | |||
| println("⚠️ Miss item: Error checking pick order line completion: ${e.message}") | |||
| e.printStackTrace() | |||
| } | |||
| // ✅ 修改:不重新建议拣货批次(因为 lot 仍然可用) | |||
| // resuggestPickOrder(request.pickOrderId) // 删除这行 | |||
| @@ -51,6 +51,8 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest | |||
| import com.ffii.fpsms.modules.stock.service.InventoryLotLineService | |||
| import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | |||
| import org.springframework.http.HttpStatus | |||
| import org.springframework.web.server.ResponseStatusException | |||
| @Serializable | |||
| data class QrContent(val itemId: Long, val stockInLineId: Long) | |||
| @@ -81,7 +83,10 @@ open class StockInLineService( | |||
| ): AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { | |||
| open fun getStockInLineInfo(stockInLineId: Long): StockInLineInfo { | |||
| return stockInLineRepository.findStockInLineInfoByIdAndDeletedFalse(stockInLineId) | |||
| // Use Optional-returning repository method to avoid EmptyResultDataAccessException | |||
| return stockInLineRepository.findStockInLineInfoById(stockInLineId).orElseThrow { | |||
| ResponseStatusException(HttpStatus.NOT_FOUND, "stockInLineId $stockInLineId not found") | |||
| } | |||
| } | |||
| open fun getReceivedStockInLineInfo(stockInLineId: Long): StockInLineInfo { | |||
| return stockInLineRepository.findStockInLineInfoByIdAndStatusAndDeletedFalse(id = stockInLineId, status = StockInLineStatus.RECEIVED.status).orElseThrow() | |||
| @@ -855,9 +855,11 @@ open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||
| @Transactional | |||
| open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineStatusByQRCodeAndLotNoRequest): MessageResponse { | |||
| val totalStartTime = System.currentTimeMillis() | |||
| val startTime = System.currentTimeMillis() | |||
| try { | |||
| println("=== QR SCAN REQUEST RECEIVED ===") | |||
| println("⏰ Request received at: ${java.time.LocalDateTime.now()}") | |||
| println(" Request details:") | |||
| println(" - stockOutLineId: ${request.stockOutLineId}") | |||
| println(" - pickOrderLineId: ${request.pickOrderLineId}") | |||
| @@ -865,10 +867,14 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| println(" - itemId: ${request.itemId}") | |||
| println(" - status: ${request.status}") | |||
| // Step 1: Find StockOutLine | |||
| val findStockOutLineStart = System.currentTimeMillis() | |||
| val stockOutLine = stockOutLineRepository.findById(request.stockOutLineId).orElseThrow { | |||
| println(" StockOutLine not found with ID: ${request.stockOutLineId}") | |||
| IllegalArgumentException("StockOutLine not found with ID: ${request.stockOutLineId}") | |||
| } | |||
| val findStockOutLineTime = System.currentTimeMillis() - findStockOutLineStart | |||
| println("⏱️ [STEP 1] Find StockOutLine: ${findStockOutLineTime}ms") | |||
| println(" Found StockOutLine:") | |||
| println(" - ID: ${stockOutLine.id}") | |||
| @@ -878,8 +884,13 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| println(" - Item ID: ${stockOutLine.item?.id}") | |||
| println(" - InventoryLotLine ID: ${stockOutLine.inventoryLotLine?.id}") | |||
| // Step 2: Get InventoryLotLine | |||
| val getInventoryLotLineStart = System.currentTimeMillis() | |||
| // 修复:从 stockOutLine.inventoryLotLine 获取 inventoryLot,而不是使用错误的参数 | |||
| val inventoryLotLine = stockOutLine.inventoryLotLine | |||
| val getInventoryLotLineTime = System.currentTimeMillis() - getInventoryLotLineStart | |||
| println("⏱️ [STEP 2] Get InventoryLotLine: ${getInventoryLotLineTime}ms") | |||
| if (inventoryLotLine == null) { | |||
| println(" StockOutLine has no associated InventoryLotLine") | |||
| return MessageResponse( | |||
| @@ -892,7 +903,12 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| ) | |||
| } | |||
| // Step 3: Get InventoryLot | |||
| val getInventoryLotStart = System.currentTimeMillis() | |||
| val inventoryLot = inventoryLotLine.inventoryLot | |||
| val getInventoryLotTime = System.currentTimeMillis() - getInventoryLotStart | |||
| println("⏱️ [STEP 3] Get InventoryLot: ${getInventoryLotTime}ms") | |||
| if (inventoryLot == null) { | |||
| println(" InventoryLotLine has no associated InventoryLot") | |||
| return MessageResponse( | |||
| @@ -911,9 +927,13 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| println(" - Item ID: ${inventoryLot.item?.id}") | |||
| println(" - InventoryLot ID: ${inventoryLot.id}") | |||
| // Step 4: Validation | |||
| val validationStart = System.currentTimeMillis() | |||
| // 修复:比较逻辑 | |||
| val lotNoMatch = inventoryLot.lotNo == request.inventoryLotNo | |||
| val itemIdMatch = inventoryLot.item?.id == request.itemId | |||
| val validationTime = System.currentTimeMillis() - validationStart | |||
| println("⏱️ [STEP 4] Validation: ${validationTime}ms") | |||
| println(" Matching results:") | |||
| println(" - Lot No match: $lotNoMatch (Expected: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo})") | |||
| @@ -923,6 +943,8 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| println(" MATCH SUCCESS: Lot and Item both match!") | |||
| // Step 5: Update StockOutLine | |||
| val updateStart = System.currentTimeMillis() | |||
| stockOutLine.status = request.status | |||
| if (request.status == "checked") { | |||
| stockOutLine.startTime = LocalDateTime.now() | |||
| @@ -930,15 +952,30 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| if (request.status == "completed") { | |||
| stockOutLine.endTime = LocalDateTime.now() | |||
| } | |||
| val updateTime = System.currentTimeMillis() - updateStart | |||
| println("⏱️ [STEP 5] Update entity: ${updateTime}ms") | |||
| // Step 6: Save to database | |||
| val saveStart = System.currentTimeMillis() | |||
| val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) | |||
| val saveTime = System.currentTimeMillis() - saveStart | |||
| println("⏱️ [STEP 6] Save to DB: ${saveTime}ms") | |||
| println(" Status updated successfully:") | |||
| println(" - New status: ${savedStockOutLine.status}") | |||
| println(" - StockOutLine ID: ${savedStockOutLine.id}") | |||
| // Step 7: Fetch mapped info | |||
| val fetchMappedStart = System.currentTimeMillis() | |||
| val mappedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) | |||
| val fetchMappedTime = System.currentTimeMillis() - fetchMappedStart | |||
| println("⏱️ [STEP 7] Fetch mapped info: ${fetchMappedTime}ms") | |||
| val totalTime = System.currentTimeMillis() - totalStartTime | |||
| println("=== QR SCAN COMPLETED SUCCESSFULLY ===") | |||
| println("⏱️ [TOTAL BACKEND TIME] ${totalTime}ms (${totalTime / 1000.0}s)") | |||
| println("📊 Breakdown: findStockOutLine=${findStockOutLineTime}ms, getInventoryLotLine=${getInventoryLotLineTime}ms, getInventoryLot=${getInventoryLotTime}ms, validation=${validationTime}ms, update=${updateTime}ms, save=${saveTime}ms, fetchMapped=${fetchMappedTime}ms") | |||
| println("⏰ Completed at: ${java.time.LocalDateTime.now()}") | |||
| return MessageResponse( | |||
| id = savedStockOutLine.id, | |||
| @@ -951,6 +988,8 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| ) | |||
| } else if (!lotNoMatch && itemIdMatch) { | |||
| // Item 匹配但 lotNo 不匹配 | |||
| val totalTime = System.currentTimeMillis() - totalStartTime | |||
| println("⏱️ [TOTAL BACKEND TIME] ${totalTime}ms (LOT MISMATCH)") | |||
| println(" LOT NUMBER MISMATCH:") | |||
| println(" - Expected lotNo: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo}") | |||
| println(" - Item ID matches: ${inventoryLot.item?.id} == ${request.itemId}") | |||
| @@ -965,6 +1004,8 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| ) | |||
| } else { | |||
| // Item 不匹配 | |||
| val totalTime = System.currentTimeMillis() - totalStartTime | |||
| println("⏱️ [TOTAL BACKEND TIME] ${totalTime}ms (ITEM MISMATCH)") | |||
| println(" ITEM MISMATCH:") | |||
| println(" - Expected itemId: ${inventoryLot.item?.id}, Got: ${request.itemId}") | |||
| println(" - Expected lotNo: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo}") | |||
| @@ -980,6 +1021,8 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| } | |||
| } catch (e: Exception) { | |||
| val totalTime = System.currentTimeMillis() - totalStartTime | |||
| println("⏱️ [TOTAL BACKEND TIME] ${totalTime}ms (ERROR)") | |||
| println(" ERROR updating stock out line status by QR: ${e.message}") | |||
| println(" Exception type: ${e.javaClass.simpleName}") | |||
| println(" Stack trace:") | |||
| @@ -26,6 +26,8 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||
| import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest | |||
| import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisRequest | |||
| import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisResponse | |||
| import org.springframework.http.HttpStatus | |||
| import org.springframework.web.server.ResponseStatusException | |||
| @RequestMapping("/inventoryLotLine") | |||
| @RestController | |||
| @@ -53,7 +55,9 @@ class InventoryLotLineController ( | |||
| @GetMapping("/lot-detail/{stockInLineId}") | |||
| fun getLotDetail(@Valid @PathVariable stockInLineId: Long): LotLineInfo { | |||
| val stockInLine = stockInLineRepository.findById(stockInLineId).orElseThrow() | |||
| val stockInLine = stockInLineRepository.findById(stockInLineId).orElseThrow { | |||
| ResponseStatusException(HttpStatus.NOT_FOUND, "stockInLineId $stockInLineId not found") | |||
| } | |||
| val inventoryLotLine = stockInLine.inventoryLotLine!! | |||
| val zero = BigDecimal.ZERO | |||
| return LotLineInfo( | |||
| @@ -52,8 +52,10 @@ class StockOutLineController( | |||
| } | |||
| @PostMapping("/updateStatusByQRCodeAndLotNo") | |||
| fun updateStatusByQRCodeAndLotNo(@Valid @RequestBody request: UpdateStockOutLineStatusByQRCodeAndLotNoRequest): MessageResponse { | |||
| val controllerStartTime = System.currentTimeMillis() | |||
| try { | |||
| println("=== CONTROLLER: updateStatusByQRCodeAndLotNo called ===") | |||
| println("⏰ Controller start time: ${java.time.LocalDateTime.now()}") | |||
| println(" Request received:") | |||
| println(" - stockOutLineId: ${request.stockOutLineId}") | |||
| println(" - pickOrderLineId: ${request.pickOrderLineId}") | |||
| @@ -61,11 +63,21 @@ class StockOutLineController( | |||
| println(" - itemId: ${request.itemId}") | |||
| println(" - status: ${request.status}") | |||
| // Measure service call time | |||
| val serviceCallStart = System.currentTimeMillis() | |||
| val result = stockOutLineService.updateStockOutLineStatusByQRCodeAndLotNo(request) | |||
| val serviceCallTime = System.currentTimeMillis() - serviceCallStart | |||
| println("⏱️ [CONTROLLER] Service call time: ${serviceCallTime}ms (${serviceCallTime / 1000.0}s)") | |||
| val controllerTotalTime = System.currentTimeMillis() - controllerStartTime | |||
| println("⏱️ [CONTROLLER] Total controller time: ${controllerTotalTime}ms (${controllerTotalTime / 1000.0}s)") | |||
| println("📊 Controller breakdown: serviceCall=${serviceCallTime}ms, overhead=${controllerTotalTime - serviceCallTime}ms") | |||
| println(" CONTROLLER: Service call completed, returning result") | |||
| println("⏰ Controller end time: ${java.time.LocalDateTime.now()}") | |||
| return result | |||
| } catch (e: Exception) { | |||
| val controllerTotalTime = System.currentTimeMillis() - controllerStartTime | |||
| println("⏱️ [CONTROLLER] Total time before error: ${controllerTotalTime}ms") | |||
| println(" CONTROLLER ERROR: ${e.message}") | |||
| println(" Exception type: ${e.javaClass.simpleName}") | |||
| e.printStackTrace() | |||