| @@ -4493,75 +4493,68 @@ 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 entities | |||
| // Validate pick order line | |||
| val pol = req.pickOrderLineId.let { pickOrderLineRepository.findById(it).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 newIll = req.newInventoryLotLineId.let { inventoryLotLineRepository.findById(it).orElse(null) } | |||
| val polItemId = pol.item?.id | |||
| if (polItemId == null) { | |||
| 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 = inventoryLotLineRepository.findByLotNoAndItemId(req.newInventoryLotNo, polItemId) | |||
| ?: return MessageResponse( | |||
| id = null, name = "New lot line not found", code = "ERROR", type = "pickorder", | |||
| message = "Inventory lot line ${req.newInventoryLotLineId} not found", errorPosition = null | |||
| message = "Inventory lot line with lotNo '${req.newInventoryLotNo}' and itemId ${polItemId} not found", | |||
| errorPosition = null | |||
| ) | |||
| // Item consistency check | |||
| val polItemId = pol.item?.id | |||
| // Item consistency check (应该已经通过上面的查询保证了,但再次确认) | |||
| val newItemId = newIll.inventoryLot?.item?.id | |||
| if (polItemId == null || newItemId == null || polItemId != newItemId) { | |||
| if (newItemId == null || polItemId != newItemId) { | |||
| 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 | |||
| ) | |||
| // 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) { | |||
| // Get current suggested ILL id and qty | |||
| val row = jdbcDao.queryForMap( | |||
| """ | |||
| SELECT spl.suggestedLotLineId AS oldIllId, COALESCE(spl.qty,0) AS qty | |||
| FROM suggested_pick_lot spl | |||
| WHERE spl.id = :splId | |||
| """.trimIndent(), mapOf("splId" to req.originalSuggestedPickLotId) | |||
| ).orElse(null) | |||
| if (row != null) { | |||
| val oldIllId = (row["oldIllId"] as Number?)?.toLong() | |||
| val qty = when (val qtyObj = row["qty"]) { | |||
| is BigDecimal -> qtyObj | |||
| is Number -> qtyObj.toDouble().toBigDecimal() | |||
| is String -> qtyObj.toBigDecimalOrNull() ?: zero | |||
| else -> zero | |||
| } | |||
| if (oldIllId != null && oldIllId != req.newInventoryLotLineId) { | |||
| // ✅ 使用 repository 而不是 SQL | |||
| val originalSpl = suggestPickLotRepository.findById(req.originalSuggestedPickLotId).orElse(null) | |||
| if (originalSpl != null) { | |||
| val oldIll = originalSpl.suggestedLotLine | |||
| val qty = originalSpl.qty ?: zero | |||
| if (oldIll != null && oldIll.id != newIllId) { | |||
| // Decrease hold on old, increase on new | |||
| val oldIll = inventoryLotLineRepository.findById(oldIllId).orElse(null) | |||
| if (oldIll != null) { | |||
| oldIll.holdQty = (oldIll.holdQty ?: zero).minus(qty).max(zero) | |||
| inventoryLotLineRepository.save(oldIll) | |||
| } | |||
| val newIllEntity = inventoryLotLineRepository.findById(req.newInventoryLotLineId).orElse(null) | |||
| if (newIllEntity != null) { | |||
| newIllEntity.holdQty = (newIllEntity.holdQty ?: zero).plus(qty) | |||
| inventoryLotLineRepository.save(newIllEntity) | |||
| } | |||
| oldIll.holdQty = (oldIll.holdQty ?: zero).minus(qty).max(zero) | |||
| inventoryLotLineRepository.save(oldIll) | |||
| newIll.holdQty = (newIll.holdQty ?: zero).plus(qty) | |||
| inventoryLotLineRepository.save(newIll) | |||
| } | |||
| // Re-point suggestion to new ILL | |||
| jdbcDao.executeUpdate( | |||
| """ | |||
| UPDATE suggested_pick_lot | |||
| SET suggestedLotLineId = :newIllId | |||
| WHERE id = :splId | |||
| """.trimIndent(), mapOf("newIllId" to req.newInventoryLotLineId, "splId" to req.originalSuggestedPickLotId) | |||
| ) | |||
| // ✅ 使用 repository 更新 suggestion | |||
| originalSpl.suggestedLotLine = newIll | |||
| suggestPickLotRepository.save(originalSpl) | |||
| } | |||
| } | |||
| // 2) Update stock out line (if provided): re-point to new ILL; keep qty and status unchanged | |||
| if (req.stockOutLineId != null && req.stockOutLineId > 0) { | |||
| val sol = stockOutLIneRepository.findById(req.stockOutLineId).orElse(null) | |||
| @@ -4571,13 +4564,13 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto | |||
| stockOutLIneRepository.save(sol) | |||
| } | |||
| } | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "Lot substitution confirmed", | |||
| code = "SUCCESS", | |||
| type = "pickorder", | |||
| message = "Updated suggestion and stock out line to new lot line ${req.newInventoryLotLineId}", | |||
| message = "Updated suggestion and stock out line to new lot line with lotNo '${req.newInventoryLotNo}'", | |||
| errorPosition = null | |||
| ) | |||
| } | |||
| @@ -5,5 +5,5 @@ data class LotSubstitutionConfirmRequest( | |||
| val pickOrderLineId: Long, | |||
| val stockOutLineId: Long?, // optional | |||
| val originalSuggestedPickLotId: Long?, // optional | |||
| val newInventoryLotLineId: Long | |||
| val newInventoryLotNo: String | |||
| ) | |||
| @@ -579,7 +579,8 @@ open class ProductProcessService( | |||
| scrapRate = bom?.scrapRate?:-1, | |||
| allergicSubstance = calculateAllergicSubstanceScore(bom?.allergicSubstances), | |||
| outputQtyUom = bom?.outputQtyUom?:"", | |||
| outputQty = bom?.outputQty?.toInt()?:0, | |||
| //outputQty = bom?.outputQty?.toInt()?:0, | |||
| outputQty = jobOrder?.reqQty?.toInt()?:0, | |||
| productProcessCode = process.productProcessCode?:"", | |||
| status = process.status?:ProductProcessStatus.PENDING, | |||
| startTime = process.startTime?:LocalDateTime.now(), | |||
| @@ -43,4 +43,13 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||
| fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: String): List<InventoryLotLine> | |||
| fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: InventoryLotLineStatus): List<InventoryLotLine> | |||
| @Query("SELECT ill FROM InventoryLotLine ill WHERE ill.inventoryLot.item.id IN :itemIds") | |||
| fun findAllByItemIdIn(@Param("itemIds") itemIds: List<Long>): List<InventoryLotLine> | |||
| @Query(""" | |||
| SELECT ill FROM InventoryLotLine ill | |||
| WHERE ill.inventoryLot.lotNo = :lotNo | |||
| AND ill.inventoryLot.item.id = :itemId | |||
| AND ill.deleted = false | |||
| """) | |||
| fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLotLine? | |||
| } | |||
| @@ -16,4 +16,11 @@ interface InventoryLotRepository: AbstractRepository<InventoryLot, Long> { | |||
| """) | |||
| fun findLatestLotNoByPrefix(prefix: String): String? | |||
| fun findAllByIdIn(ids: List<Long>): List<InventoryLot> | |||
| @Query(""" | |||
| SELECT il FROM InventoryLot il | |||
| WHERE il.lotNo = :lotNo | |||
| AND il.item.id = :itemId | |||
| AND il.deleted = false | |||
| """) | |||
| fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLot? | |||
| } | |||
| @@ -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.stock.web.model.UpdateStockOutLineStatusByQRCodeAndLotNoRequest | |||
| import com.ffii.fpsms.modules.common.CodeGenerator | |||
| import org.springframework.context.annotation.Lazy | |||
| @@ -53,6 +54,7 @@ private val doPickOrderRecordRepository: DoPickOrderRecordRepository, | |||
| private val deliveryOrderRepository: DeliveryOrderRepository, | |||
| private val doPickOrderLineRepository: DoPickOrderLineRepository, | |||
| private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository, | |||
| private val inventoryLotLineService: InventoryLotLineService | |||
| ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) { | |||
| @Throws(IOException::class) | |||
| @Transactional | |||
| @@ -686,7 +688,7 @@ open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||
| val errors = mutableListOf<String>() | |||
| val processedIds = mutableListOf<Long>() | |||
| request.lines.forEach { line -> | |||
| request.lines.forEach { line:QrPickSubmitLineRequest -> | |||
| val lineStartTime = System.currentTimeMillis() | |||
| try { | |||
| // 1) noLot 情况:等价于前端 handleSubmitAllScanned 里 noLot 分支 | |||
| @@ -920,4 +922,157 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||
| val startTime = System.currentTimeMillis() | |||
| println("=== BATCH SUBMIT START ===") | |||
| println("Start time: ${java.time.LocalDateTime.now()}") | |||
| println("Request lines count: ${request.lines.size}") | |||
| 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<String>() | |||
| val processedIds = mutableListOf<Long>() | |||
| try { | |||
| // 1) Bulk load all lot lines and inventories | |||
| val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId } | |||
| println("Loading ${lotLineIds.size} lot lines...") | |||
| val lotLines = if (lotLineIds.isNotEmpty()) { | |||
| inventoryLotLineRepository.findAllById(lotLineIds).associateBy { it.id } | |||
| } else { | |||
| emptyMap() | |||
| } | |||
| val itemIds = lotLines.values.mapNotNull { it.inventoryLot?.item?.id } | |||
| println("Loading ${itemIds.size} inventories...") | |||
| val inventories = itemIds.mapNotNull { itemId -> | |||
| inventoryRepository.findByItemId(itemId).orElse(null) | |||
| }.associateBy { it.item?.id } | |||
| // 2) Bulk load all stock out lines to get current quantities | |||
| val stockOutLineIds = request.lines.map { it.stockOutLineId } | |||
| println("Loading ${stockOutLineIds.size} stock out lines...") | |||
| val stockOutLines = stockOutLineRepository.findAllById(stockOutLineIds).associateBy { it.id } | |||
| // 3) Process each request line | |||
| request.lines.forEach { line: QrPickSubmitLineRequest -> | |||
| try { | |||
| println("Processing line: stockOutLineId=${line.stockOutLineId}, noLot=${line.noLot}") | |||
| if (line.noLot) { | |||
| // noLot branch | |||
| updateStatus(UpdateStockOutLineStatusRequest( | |||
| id = line.stockOutLineId, | |||
| status = "completed", | |||
| qty = 0.0 | |||
| )) | |||
| processedIds += line.stockOutLineId | |||
| println(" ✓ noLot item processed") | |||
| return@forEach | |||
| } | |||
| // 修复:从数据库获取当前实际数量 | |||
| val stockOutLine = stockOutLines[line.stockOutLineId] | |||
| ?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found") | |||
| // 修复:qty 是 Double?,需要转换为 BigDecimal | |||
| val currentActual = (stockOutLine.qty ?: 0.0).toBigDecimal() | |||
| val targetActual = line.actualPickQty ?: BigDecimal.ZERO | |||
| val required = line.requiredQty ?: BigDecimal.ZERO | |||
| println(" Current qty: $currentActual, Target qty: $targetActual, Required: $required") | |||
| // 修复:计算增量(前端发送的 actualPickQty 是目标累计值) | |||
| val submitQty = targetActual - currentActual | |||
| println(" Submit qty (increment): $submitQty") | |||
| // 修复:使用前端发送的状态,而不是重新计算 | |||
| val newStatus = line.stockOutLineStatus ?: if (targetActual >= required) "completed" else "partially_completed" | |||
| // 修复:updateStatus 期望增量,所以传入 submitQty | |||
| updateStatus(UpdateStockOutLineStatusRequest( | |||
| id = line.stockOutLineId, | |||
| status = newStatus, | |||
| qty = submitQty.toDouble() | |||
| )) | |||
| // Inventory updates - 修复:使用增量数量 | |||
| if (submitQty > BigDecimal.ZERO && line.inventoryLotLineId != null) { | |||
| println(" Updating inventory lot line ${line.inventoryLotLineId} with qty $submitQty") | |||
| inventoryLotLineService.updateInventoryLotLineQuantities( | |||
| UpdateInventoryLotLineQuantitiesRequest( | |||
| inventoryLotLineId = line.inventoryLotLineId, | |||
| qty = submitQty, | |||
| operation = "pick" | |||
| ) | |||
| ) | |||
| } | |||
| processedIds += line.stockOutLineId | |||
| println(" ✓ Line processed successfully") | |||
| } catch (e: Exception) { | |||
| println(" ✗ Error processing line ${line.stockOutLineId}: ${e.message}") | |||
| e.printStackTrace() | |||
| errors += "stockOutLineId=${line.stockOutLineId}: ${e.message}" | |||
| } | |||
| } | |||
| // 4) 移除:不需要保存 lotLines 和 inventories,因为它们没有被修改 | |||
| // inventoryLotLineRepository.saveAll(lotLines.values.toList()) | |||
| // inventoryRepository.saveAll(inventories.values.toList()) | |||
| 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()}") | |||
| println("=== BATCH SUBMIT END ===") | |||
| 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 | |||
| ) | |||
| ) | |||
| } catch (e: Exception) { | |||
| println("=== BATCH SUBMIT ERROR ===") | |||
| println("Error: ${e.message}") | |||
| e.printStackTrace() | |||
| return MessageResponse( | |||
| id = null, | |||
| name = "batch_submit", | |||
| code = "ERROR", | |||
| type = "batch_submit", | |||
| message = "Error: ${e.message}", | |||
| errorPosition = null, | |||
| entity = mapOf( | |||
| "processedIds" to processedIds, | |||
| "errors" to listOf(e.message ?: "Unknown error") | |||
| ) | |||
| ) | |||
| } | |||
| }} | |||
| @@ -10,11 +10,12 @@ import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineRequest | |||
| import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineRequest | |||
| import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusRequest | |||
| import jakarta.validation.Valid | |||
| import com.ffii.fpsms.modules.stock.web.model.QrPickBatchSubmitRequest | |||
| 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") | |||
| class StockOutLineController( | |||
| @@ -45,16 +46,16 @@ class StockOutLineController( | |||
| } | |||
| @PostMapping("/batchQrSubmit") | |||
| @PostMapping("/batchSubmitList") | |||
| fun batchQrSubmit(@Valid @RequestBody request: QrPickBatchSubmitRequest): MessageResponse { | |||
| return stockOutLineService.batchSubmit(request) | |||
| return stockOutLineService.newBatchSubmit(request) | |||
| } | |||
| @PostMapping("/updateStatusByQRCodeAndLotNo") | |||
| fun updateStatusByQRCodeAndLotNo(@Valid @RequestBody request: UpdateStockOutLineStatusByQRCodeAndLotNoRequest): MessageResponse { | |||
| try { | |||
| println("=== 📥 CONTROLLER: updateStatusByQRCodeAndLotNo called ===") | |||
| println("📋 Request received:") | |||
| println(" - stockOutLineId: ${request.stockOutLineId}") | |||
| println("=== CONTROLLER: updateStatusByQRCodeAndLotNo called ===") | |||
| println(" Request received:") | |||
| println(" - stockOutLineId: ${request.stockOutLineId}") | |||
| println(" - pickOrderLineId: ${request.pickOrderLineId}") | |||
| println(" - inventoryLotNo: ${request.inventoryLotNo}") | |||
| println(" - itemId: ${request.itemId}") | |||
| @@ -62,11 +63,11 @@ class StockOutLineController( | |||
| val result = stockOutLineService.updateStockOutLineStatusByQRCodeAndLotNo(request) | |||
| println("✅ CONTROLLER: Service call completed, returning result") | |||
| println(" CONTROLLER: Service call completed, returning result") | |||
| return result | |||
| } catch (e: Exception) { | |||
| println("❌ CONTROLLER ERROR: ${e.message}") | |||
| println("❌ Exception type: ${e.javaClass.simpleName}") | |||
| println(" CONTROLLER ERROR: ${e.message}") | |||
| println(" Exception type: ${e.javaClass.simpleName}") | |||
| e.printStackTrace() | |||
| // 返回错误响应而不是抛出异常 | |||
| @@ -61,6 +61,13 @@ data class UpdateStockOutLineStatusRequest( | |||
| val qty: Double? = null, | |||
| val remarks: String? = null | |||
| ) | |||
| data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( | |||
| val pickOrderLineId: Long, | |||
| val inventoryLotNo: String, | |||
| val stockOutLineId: Long, | |||
| val itemId: Long, | |||
| val status: String | |||
| ) | |||
| data class QrPickSubmitLineRequest( | |||
| val stockOutLineId: Long, | |||
| val pickOrderLineId: Long, | |||
| @@ -76,10 +83,4 @@ data class QrPickBatchSubmitRequest( | |||
| val userId: Long, | |||
| val lines: List<QrPickSubmitLineRequest> | |||
| ) | |||
| data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( | |||
| val pickOrderLineId: Long, | |||
| val inventoryLotNo: String, | |||
| val stockOutLineId: Long, | |||
| val itemId: Long, | |||
| val status: String | |||
| ) | |||