diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt index 5a382bf..7a26695 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt @@ -224,4 +224,8 @@ class DeliveryOrderController( fun printDN(@ModelAttribute request: PrintDNLabelsRequest) { deliveryOrderService.printDNLabels(request) } + @GetMapping("/batchPrintQrCode") + fun printQrCodeForDeliveryOrder(@ModelAttribute request: PrintQrCodeForDoRequest) { + stockInLineService.printQrCodeForDeliveryOrder(request) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 337c269..b496b74 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -282,7 +282,7 @@ open class JobOrderService( } jobOrderRepository.save(jo) - val pols = jo.jobms.filter { it.item?.type != "CMB"&& it.item?.type != "item"}. + val pols = jo.jobms.filter { it.item?.type != "CMB"&& it.item?.type != "consumables"}. map { SavePickOrderLineRequest( itemId = it.item?.id, diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt index e1ef0fe..dd1ac4b 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt @@ -168,7 +168,7 @@ open class TruckService( departureTime = departureTime, shopId = shop?.id!!, shopName = shopName?: "", - shopCode = shopCode, + shopCode = normalizedShopCode, loadingSequence = loadingSequence ) saveTruck(truckRequest) @@ -181,7 +181,7 @@ open class TruckService( departureTime = departureTime, shopId = shop?.id!!, shopName = shopName?: "", - shopCode = shopCode, + shopCode = normalizedShopCode, loadingSequence = loadingSequence ) saveTruck(truckRequest) diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SavePickOrderRequest.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SavePickOrderRequest.kt index aec6b0f..9e6a461 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SavePickOrderRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SavePickOrderRequest.kt @@ -20,3 +20,14 @@ data class SavePickOrderRequest ( val pickOrderLine: List ) +data class QrPickedLineRequest( + val pickOrderLineId: Long, + val inventoryLotLineId: Long, + val submitQty: BigDecimal, + val targetStatus: String +) +data class QrPickBatchSubmitRequest( + val userId: Long, + val lines: List +) + diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SearchPickOrderRequest.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SearchPickOrderRequest.kt index 6a1b0b5..e20f7da 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SearchPickOrderRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SearchPickOrderRequest.kt @@ -63,6 +63,15 @@ data class FgInfoResponse( val truckLanceCode: String?, val departureTime: LocalTime? ) +data class HierarchicalPickOrderResponse( + val fgInfo: FgInfoResponse?, + val pickOrders: List, + val totalLines: Int, + val pageNum: Int, + val pageSize: Int, + val scannedItemsCount: Int? = null, + val totalItemsCount: Int? = null +) data class PickOrderDetailResponse( val pickOrderId: Long?, @@ -131,4 +140,10 @@ data class StockOutDetailResponse( val location: String?, val availableQty: BigDecimal?, val noLot: Boolean +) + +data class DoPickOrderRequest( + val userId: Long, + val pageSize: Int?, + val pageNum: Int?, ) \ No newline at end of file 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 9e508d2..709d378 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 @@ -31,6 +31,7 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecord import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository + import com.ffii.fpsms.modules.common.CodeGenerator import org.springframework.context.annotation.Lazy @@ -46,6 +47,7 @@ open class StockOutLineService( private val pickOrderRepository: PickOrderRepository, private val inventoryLotLineRepository: InventoryLotLineRepository, @Lazy private val suggestedPickLotService: SuggestedPickLotService, + private val inventoryLotRepository: InventoryLotRepository, private val doPickOrderRepository: DoPickOrderRepository, private val doPickOrderRecordRepository: DoPickOrderRecordRepository, private val deliveryOrderRepository: DeliveryOrderRepository, @@ -665,4 +667,257 @@ private fun updateInventoryTableAfterLotRejection(inventoryLotLine: InventoryLot e.printStackTrace() } } - } \ No newline at end of file + +@Transactional(rollbackFor = [Exception::class]) +open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { + val startTime = System.currentTimeMillis() + println(" Start time: ${java.time.LocalDateTime.now()}") + if (request.lines.isEmpty()) { + return MessageResponse( + id = null, + name = "No lines", + code = "EMPTY", + type = "batch_submit", + message = "No scanned lines", + errorPosition = null + ) + } + + val errors = mutableListOf() + val processedIds = mutableListOf() + + request.lines.forEach { line -> + val lineStartTime = System.currentTimeMillis() + try { + // 1) noLot 情况:等价于前端 handleSubmitAllScanned 里 noLot 分支 + if (line.noLot) { + val req = UpdateStockOutLineStatusRequest( + id = line.stockOutLineId, + status = "completed", + qty = 0.0 + ) + updateStatus(req) // 直接复用已有逻辑(会自动更新行/订单状态) + processedIds += line.stockOutLineId + // 如果需要,这里可以再调记录 issue 的 service,而不是让前端调 + return@forEach + } + + // 2) 正常有 lot:照抄你前端 handleSubmitAllScanned 的计算逻辑 + val required = line.requiredQty ?: BigDecimal.ZERO + val currentActual = line.actualPickQty ?: BigDecimal.ZERO + val submitQty = if (line.requiredQty != null) line.requiredQty else line.requiredQty + val submitQtyNotNull = submitQty ?: BigDecimal.ZERO + val cumulative = currentActual + submitQtyNotNull + + var newStatus = "partially_completed" + if (cumulative >= required) { + newStatus = "completed" + } + + // 注意:updateStatus 是「在原 qty 上 + qty」,所以这里传的是「本次增量」而不是 cumulative + val updateReq = UpdateStockOutLineStatusRequest( + id = line.stockOutLineId, + status = newStatus, + qty = submitQtyNotNull.toDouble() + ) + updateStatus(updateReq) // 内部已经会做 pickOrderLine / pickOrder / DO 完成检查 + + // 3) 扣库存:等价于前端 updateInventoryLotLineQuantities + if (submitQtyNotNull > BigDecimal.ZERO && line.inventoryLotLineId != null) { + val lotLine = inventoryLotLineRepository.findById(line.inventoryLotLineId).orElseThrow() + val zero = BigDecimal.ZERO + val one = BigDecimal.ONE + val salesUnit = lotLine.inventoryLot?.item?.id?.let { itemId -> + itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(itemId) + } + val ratio = 1.0 //(salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one).toDouble() + val baseQty = (submitQtyNotNull.toDouble() / ratio).toBigDecimal() + + // 更新 lot line + lotLine.apply { + this.outQty = (this.outQty ?: zero) + baseQty + this.holdQty = (this.holdQty ?: zero) - baseQty + } + inventoryLotLineRepository.save(lotLine) + + // 更新 inventory + val inv = inventoryRepository.findByItemId(lotLine.inventoryLot?.item?.id!!).orElseThrow() + inv.apply { + this.onHandQty = (this.onHandQty ?: zero) - baseQty + this.onHoldQty = (this.onHoldQty ?: zero) - baseQty + } + inventoryRepository.save(inv) + } + + processedIds += line.stockOutLineId + val lineTime = System.currentTimeMillis() - lineStartTime + // 4) 如果你还想保留「按 consoCode 再单独 check-complete」的接口,也可以在这里再调一次: + // if (newStatus == "completed" && line.pickOrderConsoCode != null) { + // pickOrderService.checkAndCompletePickOrderByConsoCode(line.pickOrderConsoCode) + // } + + } catch (e: Exception) { + errors += "stockOutLineId=${line.stockOutLineId}: ${e.message}" + } + } + + val msg = if (errors.isEmpty()) { + "Batch submit success (${processedIds.size} lines)." + } else { + "Batch submit partial success (${processedIds.size} lines), errors: ${errors.joinToString("; ")}" + } + val totalTime = System.currentTimeMillis() - startTime + println(" Processed: ${processedIds.size}/${request.lines.size} items") + println(" Total time: ${totalTime}ms (${totalTime / 1000.0}s)") + println(" Average time per item: ${if (processedIds.isNotEmpty()) totalTime / processedIds.size else 0}ms") + println(" End time: ${java.time.LocalDateTime.now()}") + return MessageResponse( + id = null, + name = "batch_submit", + code = if (errors.isEmpty()) "SUCCESS" else "PARTIAL_SUCCESS", + type = "batch_submit", + message = msg, + errorPosition = null, + entity = mapOf( + "processedIds" to processedIds, + "errors" to errors + ) + ) +} + +@Transactional +open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineStatusByQRCodeAndLotNoRequest): MessageResponse { + val startTime = System.currentTimeMillis() + try { + println("=== QR SCAN REQUEST RECEIVED ===") + println(" Request details:") + println(" - stockOutLineId: ${request.stockOutLineId}") + println(" - pickOrderLineId: ${request.pickOrderLineId}") + println(" - inventoryLotNo: ${request.inventoryLotNo}") + println(" - itemId: ${request.itemId}") + println(" - status: ${request.status}") + + val stockOutLine = stockOutLineRepository.findById(request.stockOutLineId).orElseThrow { + println(" StockOutLine not found with ID: ${request.stockOutLineId}") + IllegalArgumentException("StockOutLine not found with ID: ${request.stockOutLineId}") + } + + println(" Found StockOutLine:") + println(" - ID: ${stockOutLine.id}") + println(" - Current status: ${stockOutLine.status}") + println(" - Qty: ${stockOutLine.qty}") + println(" - PickOrderLine ID: ${stockOutLine.pickOrderLine?.id}") + println(" - Item ID: ${stockOutLine.item?.id}") + println(" - InventoryLotLine ID: ${stockOutLine.inventoryLotLine?.id}") + + // 修复:从 stockOutLine.inventoryLotLine 获取 inventoryLot,而不是使用错误的参数 + val inventoryLotLine = stockOutLine.inventoryLotLine + 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 + ) + } + + val inventoryLot = inventoryLotLine.inventoryLot + if (inventoryLot == null) { + println(" InventoryLotLine has no associated InventoryLot") + return MessageResponse( + id = null, + name = "No inventory lot", + code = "NO_INVENTORY_LOT", + type = "error", + message = "InventoryLotLine ${inventoryLotLine.id} has no associated InventoryLot", + errorPosition = null + ) + } + + println("🔍 Checking inventory lot:") + println(" - Found inventory lot:") + println(" - Lot No: ${inventoryLot.lotNo}") + println(" - Item ID: ${inventoryLot.item?.id}") + println(" - InventoryLot ID: ${inventoryLot.id}") + + // 修复:比较逻辑 + val lotNoMatch = inventoryLot.lotNo == request.inventoryLotNo + val itemIdMatch = inventoryLot.item?.id == request.itemId + + println(" Matching results:") + println(" - Lot No match: $lotNoMatch (Expected: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo})") + println(" - Item ID match: $itemIdMatch (Expected: ${inventoryLot.item?.id}, Got: ${request.itemId})") + + if (lotNoMatch && itemIdMatch) { + // 匹配成功,更新状态 + println(" MATCH SUCCESS: Lot and Item both match!") + + stockOutLine.status = request.status + val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + + println(" Status updated successfully:") + println(" - New status: ${savedStockOutLine.status}") + println(" - StockOutLine ID: ${savedStockOutLine.id}") + + val mappedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) + + println("=== QR SCAN COMPLETED SUCCESSFULLY ===") + + return MessageResponse( + id = savedStockOutLine.id, + name = inventoryLot.lotNo, + code = "checked", // 修复:返回操作结果,而不是 consoPickOrderCode + type = request.status, + message = "Stock out line status updated successfully", + errorPosition = null, + entity = mappedStockOutLine + ) + } else if (!lotNoMatch && itemIdMatch) { + // Item 匹配但 lotNo 不匹配 + println(" LOT NUMBER MISMATCH:") + println(" - Expected lotNo: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo}") + println(" - Item ID matches: ${inventoryLot.item?.id} == ${request.itemId}") + + return MessageResponse( + id = null, + name = "Item match, lot number mismatch", + code = "LOT_NUMBER_MISMATCH", + type = "lot_not_found", + message = "Item match, lot number mismatch. Expected: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo}", + errorPosition = null + ) + } else { + // Item 不匹配 + println(" ITEM MISMATCH:") + println(" - Expected itemId: ${inventoryLot.item?.id}, Got: ${request.itemId}") + println(" - Expected lotNo: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo}") + + return MessageResponse( + id = null, + name = "Item mismatch", + code = "ITEM_MISMATCH", + type = "item_not_found", + message = "Item mismatch. Expected: ${inventoryLot.item?.id}, Got: ${request.itemId}", + errorPosition = null + ) + } + + } catch (e: Exception) { + println(" ERROR updating stock out line status by QR: ${e.message}") + println(" Exception type: ${e.javaClass.simpleName}") + println(" Stack trace:") + e.printStackTrace() + return MessageResponse( + id = null, + name = "Error", + code = "ERROR", + type = "error", + message = "Error updating stock out line: ${e.message}", + errorPosition = null + ) + } +} +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt index 622dd60..31654b4 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt @@ -12,6 +12,8 @@ import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusRequest import jakarta.validation.Valid import org.springframework.web.bind.annotation.* import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineWithoutConsoRequest +import com.ffii.fpsms.modules.stock.web.model.QrPickBatchSubmitRequest +import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusByQRCodeAndLotNoRequest @RestController @RequestMapping("/stockOutLine") @@ -41,4 +43,41 @@ class StockOutLineController( fun updateStatus(@Valid @RequestBody request: UpdateStockOutLineStatusRequest): MessageResponse { return stockOutLineService.updateStatus(request) } + + + @PostMapping("/batchQrSubmit") + fun batchQrSubmit(@Valid @RequestBody request: QrPickBatchSubmitRequest): MessageResponse { + return stockOutLineService.batchSubmit(request) + } + @PostMapping("/updateStatusByQRCodeAndLotNo") + fun updateStatusByQRCodeAndLotNo(@Valid @RequestBody request: UpdateStockOutLineStatusByQRCodeAndLotNoRequest): MessageResponse { + try { + println("=== 📥 CONTROLLER: updateStatusByQRCodeAndLotNo called ===") + println("📋 Request received:") + println(" - stockOutLineId: ${request.stockOutLineId}") + println(" - pickOrderLineId: ${request.pickOrderLineId}") + println(" - inventoryLotNo: ${request.inventoryLotNo}") + println(" - itemId: ${request.itemId}") + println(" - status: ${request.status}") + + val result = stockOutLineService.updateStockOutLineStatusByQRCodeAndLotNo(request) + + println("✅ CONTROLLER: Service call completed, returning result") + return result + } catch (e: Exception) { + println("❌ CONTROLLER ERROR: ${e.message}") + println("❌ Exception type: ${e.javaClass.simpleName}") + e.printStackTrace() + + // 返回错误响应而不是抛出异常 + return MessageResponse( + id = null, + name = "Controller Error", + code = "CONTROLLER_ERROR", + type = "error", + message = "Error in controller: ${e.message}", + errorPosition = null + ) + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt index cf724c9..abee9c5 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt @@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotNull import java.time.LocalDate import java.time.LocalDateTime +import java.math.BigDecimal enum class StockOutStatus(val status: String) { PENDING("pending"), COMPLETE("completed"), @@ -59,4 +60,26 @@ data class UpdateStockOutLineStatusRequest( val status: String, val qty: Double? = null, val remarks: String? = null +) +data class QrPickSubmitLineRequest( + val stockOutLineId: Long, + val pickOrderLineId: Long, + val inventoryLotLineId: Long?, + val requiredQty: BigDecimal?, + val actualPickQty: BigDecimal?, + val stockOutLineStatus: String?, + val pickOrderConsoCode: String?, + val noLot: Boolean = false +) + +data class QrPickBatchSubmitRequest( + val userId: Long, + val lines: List +) +data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( + val pickOrderLineId: Long, + val inventoryLotNo: String, + val stockOutLineId: Long, + val itemId: Long, + val status: String ) \ No newline at end of file